Просмотр исходного кода

Closes #2578: Reorganized nested serializers

Jeremy Stretch 7 лет назад
Родитель
Сommit
75f0d8ee90

+ 1 - 0
CHANGELOG.md

@@ -49,6 +49,7 @@ NetBox now supports modeling physical cables for console, power, and interface c
 * [#2572](https://github.com/digitalocean/netbox/issues/2572) - Add button to disconnect cable from circuit termination
 * [#2573](https://github.com/digitalocean/netbox/issues/2573) - Fix bulk console/power/interface disconnections
 * [#2574](https://github.com/digitalocean/netbox/issues/2574) - Remove duplicate interface links from topology maps
+* [#2578](https://github.com/digitalocean/netbox/issues/2578) - Reorganized nested serializers
 * [#2579](https://github.com/digitalocean/netbox/issues/2579) - Add missing cable disconnect buttons for front/rear ports
 
 ## API Changes

+ 52 - 0
netbox/circuits/api/nested_serializers.py

@@ -0,0 +1,52 @@
+from rest_framework import serializers
+
+from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
+from utilities.api import WritableNestedSerializer
+
+__all__ = [
+    'NestedCircuitSerializer',
+    'NestedCircuitTerminationSerializer',
+    'NestedCircuitTypeSerializer',
+    'NestedProviderSerializer',
+]
+
+
+#
+# Providers
+#
+
+class NestedProviderSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
+
+    class Meta:
+        model = Provider
+        fields = ['id', 'url', 'name', 'slug']
+
+
+#
+# Circuits
+#
+
+class NestedCircuitTypeSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
+
+    class Meta:
+        model = CircuitType
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedCircuitSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
+
+    class Meta:
+        model = Circuit
+        fields = ['id', 'url', 'cid']
+
+
+class NestedCircuitTerminationSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
+    circuit = NestedCircuitSerializer()
+
+    class Meta:
+        model = CircuitTermination
+        fields = ['id', 'url', 'circuit', 'term_side']

+ 5 - 46
netbox/circuits/api/serializers.py

@@ -1,12 +1,12 @@
-from rest_framework import serializers
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
 from circuits.constants import CIRCUIT_STATUS_CHOICES
 from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
-from dcim.api.serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
+from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
 from extras.api.customfields import CustomFieldModelSerializer
-from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
+from tenancy.api.nested_serializers import NestedTenantSerializer
+from utilities.api import ChoiceField, ValidatedModelSerializer
+from .nested_serializers import *
 
 
 #
@@ -24,16 +24,8 @@ class ProviderSerializer(TaggitSerializer, CustomFieldModelSerializer):
         ]
 
 
-class NestedProviderSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
-
-    class Meta:
-        model = Provider
-        fields = ['id', 'url', 'name', 'slug']
-
-
 #
-# Circuit types
+# Circuits
 #
 
 class CircuitTypeSerializer(ValidatedModelSerializer):
@@ -43,18 +35,6 @@ class CircuitTypeSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedCircuitTypeSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
-
-    class Meta:
-        model = CircuitType
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Circuits
-#
-
 class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
     provider = NestedProviderSerializer()
     status = ChoiceField(choices=CIRCUIT_STATUS_CHOICES, required=False)
@@ -70,18 +50,6 @@ class CircuitSerializer(TaggitSerializer, CustomFieldModelSerializer):
         ]
 
 
-class NestedCircuitSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
-
-    class Meta:
-        model = Circuit
-        fields = ['id', 'url', 'cid']
-
-
-#
-# Circuit Terminations
-#
-
 class CircuitTerminationSerializer(ValidatedModelSerializer):
     circuit = NestedCircuitSerializer()
     site = NestedSiteSerializer()
@@ -94,12 +62,3 @@ class CircuitTerminationSerializer(ValidatedModelSerializer):
             'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
             'description', 'connected_endpoint', 'cable',
         ]
-
-
-class NestedCircuitTerminationSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
-    circuit = NestedCircuitSerializer()
-
-    class Meta:
-        model = CircuitTermination
-        fields = ['id', 'url', 'circuit', 'term_side']

+ 243 - 0
netbox/dcim/api/nested_serializers.py

@@ -0,0 +1,243 @@
+from rest_framework import serializers
+
+from dcim.models import (
+    Cable, ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceType, DeviceRole, FrontPort, FrontPortTemplate,
+    Interface, Manufacturer, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, RearPort, RearPortTemplate,
+    Region, Site, VirtualChassis,
+)
+from utilities.api import WritableNestedSerializer
+
+__all__ = [
+    'NestedCableSerializer',
+    'NestedConsolePortSerializer',
+    'NestedConsoleServerPortSerializer',
+    'NestedDeviceBaySerializer',
+    'NestedDeviceRoleSerializer',
+    'NestedDeviceSerializer',
+    'NestedDeviceTypeSerializer',
+    'NestedFrontPortSerializer',
+    'NestedFrontPortTemplateSerializer',
+    'NestedInterfaceSerializer',
+    'NestedManufacturerSerializer',
+    'NestedPlatformSerializer',
+    'NestedPowerOutletSerializer',
+    'NestedPowerPortSerializer',
+    'NestedRackGroupSerializer',
+    'NestedRackRoleSerializer',
+    'NestedRackSerializer',
+    'NestedRearPortSerializer',
+    'NestedRearPortTemplateSerializer',
+    'NestedRegionSerializer',
+    'NestedSiteSerializer',
+    'NestedVirtualChassisSerializer',
+]
+
+
+#
+# Regions/sites
+#
+
+class NestedRegionSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
+
+    class Meta:
+        model = Region
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedSiteSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
+
+    class Meta:
+        model = Site
+        fields = ['id', 'url', 'name', 'slug']
+
+
+#
+# Racks
+#
+
+class NestedRackGroupSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
+
+    class Meta:
+        model = RackGroup
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedRackRoleSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
+
+    class Meta:
+        model = RackRole
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedRackSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
+
+    class Meta:
+        model = Rack
+        fields = ['id', 'url', 'name', 'display_name']
+
+
+#
+# Device types
+#
+
+class NestedManufacturerSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
+
+    class Meta:
+        model = Manufacturer
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedDeviceTypeSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
+    manufacturer = NestedManufacturerSerializer(read_only=True)
+
+    class Meta:
+        model = DeviceType
+        fields = ['id', 'url', 'manufacturer', 'model', 'slug']
+
+
+class NestedRearPortTemplateSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
+
+    class Meta:
+        model = RearPortTemplate
+        fields = ['id', 'url', 'name']
+
+
+class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
+
+    class Meta:
+        model = FrontPortTemplate
+        fields = ['id', 'url', 'name']
+
+
+#
+# Devices
+#
+
+class NestedDeviceRoleSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
+
+    class Meta:
+        model = DeviceRole
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedPlatformSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
+
+    class Meta:
+        model = Platform
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedDeviceSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
+
+    class Meta:
+        model = Device
+        fields = ['id', 'url', 'name', 'display_name']
+
+
+class NestedConsoleServerPortSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
+    device = NestedDeviceSerializer(read_only=True)
+
+    class Meta:
+        model = ConsoleServerPort
+        fields = ['id', 'url', 'device', 'name', 'cable']
+
+
+class NestedConsolePortSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
+    device = NestedDeviceSerializer(read_only=True)
+
+    class Meta:
+        model = ConsolePort
+        fields = ['id', 'url', 'device', 'name', 'cable']
+
+
+class NestedPowerOutletSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
+    device = NestedDeviceSerializer(read_only=True)
+
+    class Meta:
+        model = PowerOutlet
+        fields = ['id', 'url', 'device', 'name', 'cable']
+
+
+class NestedPowerPortSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
+    device = NestedDeviceSerializer(read_only=True)
+
+    class Meta:
+        model = PowerPort
+        fields = ['id', 'url', 'device', 'name', 'cable']
+
+
+class NestedInterfaceSerializer(WritableNestedSerializer):
+    device = NestedDeviceSerializer(read_only=True)
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
+
+    class Meta:
+        model = Interface
+        fields = ['id', 'url', 'device', 'name', 'cable']
+
+
+class NestedRearPortSerializer(WritableNestedSerializer):
+    device = NestedDeviceSerializer(read_only=True)
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
+
+    class Meta:
+        model = RearPort
+        fields = ['id', 'url', 'device', 'name', 'cable']
+
+
+class NestedFrontPortSerializer(WritableNestedSerializer):
+    device = NestedDeviceSerializer(read_only=True)
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
+
+    class Meta:
+        model = FrontPort
+        fields = ['id', 'url', 'device', 'name', 'cable']
+
+
+class NestedDeviceBaySerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
+    device = NestedDeviceSerializer(read_only=True)
+
+    class Meta:
+        model = DeviceBay
+        fields = ['id', 'url', 'device', 'name']
+
+
+#
+# Cables
+#
+
+class NestedCableSerializer(serializers.ModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
+
+    class Meta:
+        model = Cable
+        fields = ['id', 'url', 'label']
+
+
+#
+# Virtual chassis
+#
+
+class NestedVirtualChassisSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
+    master = NestedDeviceSerializer()
+
+    class Meta:
+        model = VirtualChassis
+        fields = ['id', 'url', 'master']

+ 78 - 409
netbox/dcim/api/serializers.py

@@ -2,7 +2,6 @@ from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
-from circuits.models import Circuit, CircuitTermination
 from dcim.constants import *
 from dcim.models import (
     Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
@@ -11,28 +10,22 @@ from dcim.models import (
     RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, VirtualChassis,
 )
 from extras.api.customfields import CustomFieldModelSerializer
-from ipam.models import IPAddress, VLAN
-from tenancy.api.serializers import NestedTenantSerializer
-from users.api.serializers import NestedUserSerializer
+from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
+from ipam.models import VLAN
+from tenancy.api.nested_serializers import NestedTenantSerializer
+from users.api.nested_serializers import NestedUserSerializer
 from utilities.api import (
     ChoiceField, ContentTypeField, SerializedPKRelatedField, TimeZoneField, ValidatedModelSerializer,
     WritableNestedSerializer, get_serializer_for_model,
 )
-from virtualization.models import Cluster
+from virtualization.api.nested_serializers import NestedClusterSerializer
+from .nested_serializers import *
 
 
 #
-# Regions
+# Regions/sites
 #
 
-class NestedRegionSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
-
-    class Meta:
-        model = Region
-        fields = ['id', 'url', 'name', 'slug']
-
-
 class RegionSerializer(serializers.ModelSerializer):
     parent = NestedRegionSerializer(required=False, allow_null=True)
 
@@ -41,10 +34,6 @@ class RegionSerializer(serializers.ModelSerializer):
         fields = ['id', 'name', 'slug', 'parent']
 
 
-#
-# Sites
-#
-
 class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
     status = ChoiceField(choices=SITE_STATUS_CHOICES, required=False)
     region = NestedRegionSerializer(required=False, allow_null=True)
@@ -62,16 +51,8 @@ class SiteSerializer(TaggitSerializer, CustomFieldModelSerializer):
         ]
 
 
-class NestedSiteSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
-
-    class Meta:
-        model = Site
-        fields = ['id', 'url', 'name', 'slug']
-
-
 #
-# Rack groups
+# Racks
 #
 
 class RackGroupSerializer(ValidatedModelSerializer):
@@ -82,18 +63,6 @@ class RackGroupSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'site']
 
 
-class NestedRackGroupSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail')
-
-    class Meta:
-        model = RackGroup
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Rack roles
-#
-
 class RackRoleSerializer(ValidatedModelSerializer):
 
     class Meta:
@@ -101,18 +70,6 @@ class RackRoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'color']
 
 
-class NestedRackRoleSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
-
-    class Meta:
-        model = RackRole
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Racks
-#
-
 class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
     site = NestedSiteSerializer()
     group = NestedRackGroupSerializer(required=False, allow_null=True, default=None)
@@ -151,26 +108,6 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer):
         return data
 
 
-class NestedRackSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
-
-    class Meta:
-        model = Rack
-        fields = ['id', 'url', 'name', 'display_name']
-
-
-#
-# Rack units
-#
-
-class NestedDeviceSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
-
-    class Meta:
-        model = Device
-        fields = ['id', 'url', 'name', 'display_name']
-
-
 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.
@@ -181,10 +118,6 @@ class RackUnitSerializer(serializers.Serializer):
     device = NestedDeviceSerializer(read_only=True)
 
 
-#
-# Rack reservations
-#
-
 class RackReservationSerializer(ValidatedModelSerializer):
     rack = NestedRackSerializer()
     user = NestedUserSerializer()
@@ -196,7 +129,7 @@ class RackReservationSerializer(ValidatedModelSerializer):
 
 
 #
-# Manufacturers
+# Device types
 #
 
 class ManufacturerSerializer(ValidatedModelSerializer):
@@ -206,18 +139,6 @@ class ManufacturerSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedManufacturerSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
-
-    class Meta:
-        model = Manufacturer
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Device types
-#
-
 class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
     manufacturer = NestedManufacturerSerializer()
     subdevice_role = ChoiceField(choices=SUBDEVICE_ROLE_CHOICES, required=False, allow_null=True)
@@ -232,19 +153,6 @@ class DeviceTypeSerializer(TaggitSerializer, CustomFieldModelSerializer):
         ]
 
 
-class NestedDeviceTypeSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
-    manufacturer = NestedManufacturerSerializer(read_only=True)
-
-    class Meta:
-        model = DeviceType
-        fields = ['id', 'url', 'manufacturer', 'model', 'slug']
-
-
-#
-# Console port templates
-#
-
 class ConsolePortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
@@ -253,10 +161,6 @@ class ConsolePortTemplateSerializer(ValidatedModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-#
-# Console server port templates
-#
-
 class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
@@ -265,10 +169,6 @@ class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-#
-# Power port templates
-#
-
 class PowerPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
@@ -277,10 +177,6 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-#
-# Power outlet templates
-#
-
 class PowerOutletTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
@@ -289,10 +185,6 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
         fields = ['id', 'device_type', 'name']
 
 
-#
-# Interface templates
-#
-
 class InterfaceTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
@@ -302,10 +194,6 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
         fields = ['id', 'device_type', 'name', 'form_factor', 'mgmt_only']
 
 
-#
-# Rear port templates
-#
-
 class RearPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     type = ChoiceField(choices=PORT_TYPE_CHOICES)
@@ -315,18 +203,6 @@ class RearPortTemplateSerializer(ValidatedModelSerializer):
         fields = ['id', 'device_type', 'name', 'type', 'positions']
 
 
-class NestedRearPortTemplateSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
-
-    class Meta:
-        model = RearPortTemplate
-        fields = ['id', 'url', 'name']
-
-
-#
-# Front port templates
-#
-
 class FrontPortTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     type = ChoiceField(choices=PORT_TYPE_CHOICES)
@@ -337,18 +213,6 @@ class FrontPortTemplateSerializer(ValidatedModelSerializer):
         fields = ['id', 'device_type', 'name', 'type', 'rear_port', 'rear_port_position']
 
 
-class NestedFrontPortTemplateSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
-
-    class Meta:
-        model = FrontPortTemplate
-        fields = ['id', 'url', 'name']
-
-
-#
-# Device bay templates
-#
-
 class DeviceBayTemplateSerializer(ValidatedModelSerializer):
     device_type = NestedDeviceTypeSerializer()
 
@@ -358,7 +222,7 @@ class DeviceBayTemplateSerializer(ValidatedModelSerializer):
 
 
 #
-# Device roles
+# Devices
 #
 
 class DeviceRoleSerializer(ValidatedModelSerializer):
@@ -368,18 +232,6 @@ class DeviceRoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'color', 'vm_role']
 
 
-class NestedDeviceRoleSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
-
-    class Meta:
-        model = DeviceRole
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Platforms
-#
-
 class PlatformSerializer(ValidatedModelSerializer):
     manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
 
@@ -388,46 +240,6 @@ class PlatformSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args']
 
 
-class NestedPlatformSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
-
-    class Meta:
-        model = Platform
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Devices
-#
-
-# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
-class DeviceIPAddressSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
-
-    class Meta:
-        model = IPAddress
-        fields = ['id', 'url', 'family', 'address']
-
-
-# Cannot import virtualization.api.NestedClusterSerializer due to circular dependency
-class NestedClusterSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
-
-    class Meta:
-        model = Cluster
-        fields = ['id', 'url', 'name']
-
-
-# Cannot import NestedVirtualChassisSerializer due to circular dependency
-class DeviceVirtualChassisSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
-    master = NestedDeviceSerializer()
-
-    class Meta:
-        model = VirtualChassis
-        fields = ['id', 'url', 'master']
-
-
 class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
     device_type = NestedDeviceTypeSerializer()
     device_role = NestedDeviceRoleSerializer()
@@ -437,12 +249,12 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
     rack = NestedRackSerializer(required=False, allow_null=True)
     face = ChoiceField(choices=RACK_FACE_CHOICES, required=False, allow_null=True)
     status = ChoiceField(choices=DEVICE_STATUS_CHOICES, required=False)
-    primary_ip = DeviceIPAddressSerializer(read_only=True)
-    primary_ip4 = DeviceIPAddressSerializer(required=False, allow_null=True)
-    primary_ip6 = DeviceIPAddressSerializer(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)
     parent_device = serializers.SerializerMethodField()
     cluster = NestedClusterSerializer(required=False, allow_null=True)
-    virtual_chassis = DeviceVirtualChassisSerializer(required=False, allow_null=True)
+    virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True)
     tags = TagListSerializerField(required=False)
 
     class Meta:
@@ -450,8 +262,8 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer):
         fields = [
             'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
-            'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created',
-            'last_updated', 'local_context_data',
+            'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags',
+            'custom_fields', 'created', 'last_updated',
         ]
         validators = []
 
@@ -486,80 +298,14 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
         fields = [
             'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6',
-            'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields',
-            'config_context', 'created', 'last_updated', 'local_context_data',
+            'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'local_context_data', 'tags',
+            'custom_fields', 'config_context', 'created', 'last_updated',
         ]
 
     def get_config_context(self, obj):
         return obj.get_config_context()
 
 
-#
-# Cables
-#
-
-class CableSerializer(ValidatedModelSerializer):
-    termination_a_type = ContentTypeField()
-    termination_b_type = ContentTypeField()
-    termination_a = serializers.SerializerMethodField(read_only=True)
-    termination_b = serializers.SerializerMethodField(read_only=True)
-    status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
-    length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False)
-
-    class Meta:
-        model = Cable
-        fields = [
-            'id', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id',
-            'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit',
-        ]
-
-    def _get_termination(self, obj, side):
-        """
-        Serialize a nested representation of a termination.
-        """
-        if side.lower() not in ['a', 'b']:
-            raise ValueError("Termination side must be either A or B.")
-        termination = getattr(obj, 'termination_{}'.format(side.lower()))
-        if termination is None:
-            return None
-        serializer = get_serializer_for_model(termination, prefix='Nested')
-        context = {'request': self.context['request']}
-        data = serializer(termination, context=context).data
-
-        return data
-
-    def get_termination_a(self, obj):
-        return self._get_termination(obj, 'a')
-
-    def get_termination_b(self, obj):
-        return self._get_termination(obj, 'b')
-
-
-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',
-        ]
-
-
-class NestedCableSerializer(serializers.ModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
-
-    class Meta:
-        model = Cable
-        fields = ['id', 'url', 'label']
-
-
-#
-# Console server ports
-#
-
 class ConsoleServerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     cable = NestedCableSerializer(read_only=True)
@@ -571,19 +317,6 @@ class ConsoleServerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
         read_only_fields = ['connected_endpoint']
 
 
-class NestedConsoleServerPortSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
-    device = NestedDeviceSerializer(read_only=True)
-
-    class Meta:
-        model = ConsoleServerPort
-        fields = ['id', 'url', 'device', 'name', 'cable']
-
-
-#
-# Console ports
-#
-
 class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     connected_endpoint = NestedConsoleServerPortSerializer(read_only=True)
@@ -595,19 +328,6 @@ class ConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
         fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
 
 
-class NestedConsolePortSerializer(TaggitSerializer, ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
-    device = NestedDeviceSerializer(read_only=True)
-
-    class Meta:
-        model = ConsolePort
-        fields = ['id', 'url', 'device', 'name', 'cable']
-
-
-#
-# Power outlets
-#
-
 class PowerOutletSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     cable = NestedCableSerializer(read_only=True)
@@ -619,19 +339,6 @@ class PowerOutletSerializer(TaggitSerializer, ValidatedModelSerializer):
         read_only_fields = ['connected_endpoint']
 
 
-class NestedPowerOutletSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
-    device = NestedDeviceSerializer(read_only=True)
-
-    class Meta:
-        model = PowerOutlet
-        fields = ['id', 'url', 'device', 'name', 'cable']
-
-
-#
-# Power ports
-#
-
 class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     connected_endpoint = NestedPowerOutletSerializer(read_only=True)
@@ -643,65 +350,16 @@ class PowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
         fields = ['id', 'device', 'name', 'connected_endpoint', 'connection_status', 'cable', 'tags']
 
 
-class NestedPowerPortSerializer(TaggitSerializer, ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
-    device = NestedDeviceSerializer(read_only=True)
-
-    class Meta:
-        model = PowerPort
-        fields = ['id', 'url', 'device', 'name', 'cable']
-
-
-#
-# Interfaces
-#
-
-class NestedInterfaceSerializer(WritableNestedSerializer):
-    device = NestedDeviceSerializer(read_only=True)
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
-
-    class Meta:
-        model = Interface
-        fields = ['id', 'url', 'device', 'name', 'cable']
-
-
-class InterfaceNestedCircuitSerializer(serializers.ModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
-
-    class Meta:
-        model = Circuit
-        fields = ['id', 'url', 'cid']
-
-
-class InterfaceCircuitTerminationSerializer(WritableNestedSerializer):
-    circuit = InterfaceNestedCircuitSerializer(read_only=True)
-
-    class Meta:
-        model = CircuitTermination
-        fields = [
-            'id', 'circuit', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
-        ]
-
-
-# Cannot import ipam.api.NestedVLANSerializer due to circular dependency
-class InterfaceVLANSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
-
-    class Meta:
-        model = VLAN
-        fields = ['id', 'url', 'vid', 'name', 'display_name']
-
-
 class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
     lag = NestedInterfaceSerializer(required=False, allow_null=True)
     connected_endpoint = serializers.SerializerMethodField(read_only=True)
     mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
-    untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
+    untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     tagged_vlans = SerializedPKRelatedField(
         queryset=VLAN.objects.all(),
-        serializer=InterfaceVLANSerializer,
+        serializer=NestedVLANSerializer,
         required=False,
         many=True
     )
@@ -715,6 +373,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
             'connected_endpoint', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
         ]
 
+    # TODO: This validation should be handled by Interface.clean()
     def validate(self, data):
 
         # All associated VLANs be global or assigned to the parent device's site.
@@ -748,10 +407,6 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
         return data
 
 
-#
-# Rear ports
-#
-
 class RearPortSerializer(ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     type = ChoiceField(choices=PORT_TYPE_CHOICES)
@@ -763,20 +418,10 @@ class RearPortSerializer(ValidatedModelSerializer):
         fields = ['id', 'device', 'name', 'type', 'positions', 'cable', 'tags']
 
 
-class NestedRearPortSerializer(WritableNestedSerializer):
-    device = NestedDeviceSerializer(read_only=True)
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
-
-    class Meta:
-        model = RearPort
-        fields = ['id', 'url', 'device', 'name', 'cable']
-
-
-#
-# Front ports
-#
-
 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:
@@ -796,19 +441,6 @@ class FrontPortSerializer(ValidatedModelSerializer):
         fields = ['id', 'device', 'name', 'type', 'rear_port', 'rear_port_position', 'cable', 'tags']
 
 
-class NestedFrontPortSerializer(WritableNestedSerializer):
-    device = NestedDeviceSerializer(read_only=True)
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
-
-    class Meta:
-        model = FrontPort
-        fields = ['id', 'url', 'device', 'name', 'cable']
-
-
-#
-# Device bays
-#
-
 class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
     device = NestedDeviceSerializer()
     installed_device = NestedDeviceSerializer(required=False, allow_null=True)
@@ -819,15 +451,6 @@ class DeviceBaySerializer(TaggitSerializer, ValidatedModelSerializer):
         fields = ['id', 'device', 'name', 'installed_device', 'tags']
 
 
-class NestedDeviceBaySerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
-    device = NestedDeviceSerializer(read_only=True)
-
-    class Meta:
-        model = DeviceBay
-        fields = ['id', 'url', 'device', 'name']
-
-
 #
 # Inventory items
 #
@@ -847,6 +470,60 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
         ]
 
 
+#
+# Cables
+#
+
+class CableSerializer(ValidatedModelSerializer):
+    termination_a_type = ContentTypeField()
+    termination_b_type = ContentTypeField()
+    termination_a = serializers.SerializerMethodField(read_only=True)
+    termination_b = serializers.SerializerMethodField(read_only=True)
+    status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
+    length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False)
+
+    class Meta:
+        model = Cable
+        fields = [
+            'id', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id',
+            'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit',
+        ]
+
+    def _get_termination(self, obj, side):
+        """
+        Serialize a nested representation of a termination.
+        """
+        if side.lower() not in ['a', 'b']:
+            raise ValueError("Termination side must be either A or B.")
+        termination = getattr(obj, 'termination_{}'.format(side.lower()))
+        if termination is None:
+            return None
+        serializer = get_serializer_for_model(termination, prefix='Nested')
+        context = {'request': self.context['request']}
+        data = serializer(termination, context=context).data
+
+        return data
+
+    def get_termination_a(self, obj):
+        return self._get_termination(obj, 'a')
+
+    def get_termination_b(self, obj):
+        return self._get_termination(obj, 'b')
+
+
+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',
+        ]
+
+
 #
 # Interface connections
 #
@@ -876,11 +553,3 @@ class VirtualChassisSerializer(TaggitSerializer, ValidatedModelSerializer):
     class Meta:
         model = VirtualChassis
         fields = ['id', 'master', 'domain', 'tags']
-
-
-class NestedVirtualChassisSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
-
-    class Meta:
-        model = VirtualChassis
-        fields = ['id', 'url']

+ 1 - 1
netbox/dcim/tests/test_api.py

@@ -3433,7 +3433,7 @@ class VirtualChassisTest(APITestCase):
 
         self.assertEqual(
             sorted(response.data['results'][0]),
-            ['id', 'url']
+            ['id', 'master', 'url']
         )
 
     def test_create_virtualchassis(self):

+ 23 - 0
netbox/extras/api/nested_serializers.py

@@ -0,0 +1,23 @@
+from rest_framework import serializers
+
+from extras.models import ReportResult
+
+__all__ = [
+    'NestedReportResultSerializer',
+]
+
+
+#
+# Reports
+#
+
+class NestedReportResultSerializer(serializers.ModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='extras-api:report-detail',
+        lookup_field='report',
+        lookup_url_kwarg='pk'
+    )
+
+    class Meta:
+        model = ReportResult
+        fields = ['url', 'created', 'user', 'failed']

+ 4 - 15
netbox/extras/api/serializers.py

@@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist
 from rest_framework import serializers
 from taggit.models import Tag
 
-from dcim.api.serializers import (
+from dcim.api.nested_serializers import (
     NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
     NestedRegionSerializer, NestedSiteSerializer,
 )
@@ -11,12 +11,13 @@ from extras.constants import *
 from extras.models import (
     ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, TopologyMap,
 )
-from tenancy.api.serializers import NestedTenantSerializer, NestedTenantGroupSerializer
+from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
 from tenancy.models import Tenant, TenantGroup
-from users.api.serializers import NestedUserSerializer
+from users.api.nested_serializers import NestedUserSerializer
 from utilities.api import (
     ChoiceField, ContentTypeField, get_serializer_for_model, SerializedPKRelatedField, ValidatedModelSerializer,
 )
+from .nested_serializers import *
 
 
 #
@@ -187,18 +188,6 @@ class ReportResultSerializer(serializers.ModelSerializer):
         fields = ['created', 'user', 'failed', 'data']
 
 
-class NestedReportResultSerializer(serializers.ModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='extras-api:report-detail',
-        lookup_field='report',
-        lookup_url_kwarg='pk'
-    )
-
-    class Meta:
-        model = ReportResult
-        fields = ['url', 'created', 'user', 'failed']
-
-
 class ReportSerializer(serializers.Serializer):
     module = serializers.CharField(max_length=255)
     name = serializers.CharField(max_length=255)

+ 100 - 0
netbox/ipam/api/nested_serializers.py

@@ -0,0 +1,100 @@
+from rest_framework import serializers
+
+from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
+from utilities.api import WritableNestedSerializer
+
+__all__ = [
+    'NestedAggregateSerializer',
+    'NestedIPAddressSerializer',
+    'NestedPrefixSerializer',
+    'NestedRIRSerializer',
+    'NestedRoleSerializer',
+    'NestedVLANGroupSerializer',
+    'NestedVLANSerializer',
+    'NestedVRFSerializer',
+]
+
+
+#
+# VRFs
+#
+
+class NestedVRFSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
+
+    class Meta:
+        model = VRF
+        fields = ['id', 'url', 'name', 'rd']
+
+
+#
+# RIRs/aggregates
+#
+
+class NestedRIRSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
+
+    class Meta:
+        model = RIR
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedAggregateSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
+
+    class Meta:
+        model = Aggregate
+        fields = ['id', 'url', 'family', 'prefix']
+
+
+#
+# VLANs
+#
+
+class NestedRoleSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
+
+    class Meta:
+        model = Role
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedVLANGroupSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
+
+    class Meta:
+        model = VLANGroup
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedVLANSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
+
+    class Meta:
+        model = VLAN
+        fields = ['id', 'url', 'vid', 'name', 'display_name']
+
+
+#
+# Prefixes
+#
+
+class NestedPrefixSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
+
+    class Meta:
+        model = Prefix
+        fields = ['id', 'url', 'family', 'prefix']
+
+
+#
+# IP addresses
+#
+
+
+class NestedIPAddressSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
+
+    class Meta:
+        model = IPAddress
+        fields = ['id', 'url', 'family', 'address']

+ 25 - 97
netbox/ipam/api/serializers.py

@@ -5,18 +5,17 @@ from rest_framework.reverse import reverse
 from rest_framework.validators import UniqueTogetherValidator
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
-from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
+from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
 from dcim.models import Interface
 from extras.api.customfields import CustomFieldModelSerializer
-from ipam.constants import (
-    IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
-)
+from ipam.constants import *
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
-from tenancy.api.serializers import NestedTenantSerializer
+from tenancy.api.nested_serializers import NestedTenantSerializer
 from utilities.api import (
     ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer,
 )
-from virtualization.api.serializers import NestedVirtualMachineSerializer
+from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
+from .nested_serializers import *
 
 
 #
@@ -35,35 +34,8 @@ class VRFSerializer(TaggitSerializer, CustomFieldModelSerializer):
         ]
 
 
-class NestedVRFSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
-
-    class Meta:
-        model = VRF
-        fields = ['id', 'url', 'name', 'rd']
-
-
-#
-# Roles
-#
-
-class RoleSerializer(ValidatedModelSerializer):
-
-    class Meta:
-        model = Role
-        fields = ['id', 'name', 'slug', 'weight']
-
-
-class NestedRoleSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
-
-    class Meta:
-        model = Role
-        fields = ['id', 'url', 'name', 'slug']
-
-
 #
-# RIRs
+# RIRs/aggregates
 #
 
 class RIRSerializer(ValidatedModelSerializer):
@@ -73,18 +45,6 @@ class RIRSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug', 'is_private']
 
 
-class NestedRIRSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
-
-    class Meta:
-        model = RIR
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Aggregates
-#
-
 class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
     rir = NestedRIRSerializer()
     tags = TagListSerializerField(required=False)
@@ -98,17 +58,16 @@ class AggregateSerializer(TaggitSerializer, CustomFieldModelSerializer):
         read_only_fields = ['family']
 
 
-class NestedAggregateSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
+#
+# VLANs
+#
 
-    class Meta(AggregateSerializer.Meta):
-        model = Aggregate
-        fields = ['id', 'url', 'family', 'prefix']
+class RoleSerializer(ValidatedModelSerializer):
 
+    class Meta:
+        model = Role
+        fields = ['id', 'name', 'slug', 'weight']
 
-#
-# VLAN groups
-#
 
 class VLANGroupSerializer(ValidatedModelSerializer):
     site = NestedSiteSerializer(required=False, allow_null=True)
@@ -133,18 +92,6 @@ class VLANGroupSerializer(ValidatedModelSerializer):
         return data
 
 
-class NestedVLANGroupSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
-
-    class Meta:
-        model = VLANGroup
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# VLANs
-#
-
 class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
     site = NestedSiteSerializer(required=False, allow_null=True)
     group = NestedVLANGroupSerializer(required=False, allow_null=True)
@@ -176,14 +123,6 @@ class VLANSerializer(TaggitSerializer, CustomFieldModelSerializer):
         return data
 
 
-class NestedVLANSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
-
-    class Meta:
-        model = VLAN
-        fields = ['id', 'url', 'vid', 'name', 'display_name']
-
-
 #
 # Prefixes
 #
@@ -206,16 +145,10 @@ class PrefixSerializer(TaggitSerializer, CustomFieldModelSerializer):
         read_only_fields = ['family']
 
 
-class NestedPrefixSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
-
-    class Meta:
-        model = Prefix
-        fields = ['id', 'url', 'family', 'prefix']
-
-
 class AvailablePrefixSerializer(serializers.Serializer):
-
+    """
+    Representation of a prefix which does not exist in the database.
+    """
     def to_representation(self, instance):
         if self.context.get('vrf'):
             vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
@@ -233,11 +166,14 @@ class AvailablePrefixSerializer(serializers.Serializer):
 #
 
 class IPAddressInterfaceSerializer(WritableNestedSerializer):
+    """
+    Nested representation of an Interface which may belong to a Device *or* a VirtualMachine.
+    """
     url = serializers.SerializerMethodField()  # We're imitating a HyperlinkedIdentityField here
     device = NestedDeviceSerializer(read_only=True)
     virtual_machine = NestedVirtualMachineSerializer(read_only=True)
 
-    class Meta(InterfaceSerializer.Meta):
+    class Meta:
         model = Interface
         fields = [
             'id', 'url', 'device', 'virtual_machine', 'name',
@@ -258,6 +194,8 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
     status = ChoiceField(choices=IPADDRESS_STATUS_CHOICES, required=False)
     role = ChoiceField(choices=IPADDRESS_ROLE_CHOICES, required=False, allow_null=True)
     interface = IPAddressInterfaceSerializer(required=False, allow_null=True)
+    nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
+    nat_outside = NestedIPAddressSerializer(read_only=True)
     tags = TagListSerializerField(required=False)
 
     class Meta:
@@ -269,20 +207,10 @@ class IPAddressSerializer(TaggitSerializer, CustomFieldModelSerializer):
         read_only_fields = ['family']
 
 
-class NestedIPAddressSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
-
-    class Meta:
-        model = IPAddress
-        fields = ['id', 'url', 'family', 'address']
-
-
-IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer(required=False, allow_null=True)
-IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer(read_only=True)
-
-
 class AvailableIPSerializer(serializers.Serializer):
-
+    """
+    Representation of an IP address which does not exist in the database.
+    """
     def to_representation(self, instance):
         if self.context.get('vrf'):
             vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data

+ 16 - 0
netbox/secrets/api/nested_serializers.py

@@ -0,0 +1,16 @@
+from rest_framework import serializers
+
+from secrets.models import SecretRole
+from utilities.api import WritableNestedSerializer
+
+__all__ = [
+    'NestedSecretRoleSerializer'
+]
+
+
+class NestedSecretRoleSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
+
+    class Meta:
+        model = SecretRole
+        fields = ['id', 'url', 'name', 'slug']

+ 4 - 15
netbox/secrets/api/serializers.py

@@ -2,14 +2,15 @@ from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
-from dcim.api.serializers import NestedDeviceSerializer
+from dcim.api.nested_serializers import NestedDeviceSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from secrets.models import Secret, SecretRole
-from utilities.api import ValidatedModelSerializer, WritableNestedSerializer
+from utilities.api import ValidatedModelSerializer
+from .nested_serializers import *
 
 
 #
-# SecretRoles
+# Secrets
 #
 
 class SecretRoleSerializer(ValidatedModelSerializer):
@@ -19,18 +20,6 @@ class SecretRoleSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedSecretRoleSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail')
-
-    class Meta:
-        model = SecretRole
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Secrets
-#
-
 class SecretSerializer(TaggitSerializer, CustomFieldModelSerializer):
     device = NestedDeviceSerializer()
     role = NestedSecretRoleSerializer()

+ 29 - 0
netbox/tenancy/api/nested_serializers.py

@@ -0,0 +1,29 @@
+from rest_framework import serializers
+
+from tenancy.models import Tenant, TenantGroup
+from utilities.api import WritableNestedSerializer
+
+__all__ = [
+    'NestedTenantGroupSerializer',
+    'NestedTenantSerializer',
+]
+
+
+#
+# Tenants
+#
+
+class NestedTenantGroupSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
+
+    class Meta:
+        model = TenantGroup
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedTenantSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
+
+    class Meta:
+        model = Tenant
+        fields = ['id', 'url', 'name', 'slug']

+ 3 - 23
netbox/tenancy/api/serializers.py

@@ -1,13 +1,13 @@
-from rest_framework import serializers
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
 from extras.api.customfields import CustomFieldModelSerializer
 from tenancy.models import Tenant, TenantGroup
-from utilities.api import ValidatedModelSerializer, WritableNestedSerializer
+from utilities.api import ValidatedModelSerializer
+from .nested_serializers import *
 
 
 #
-# Tenant groups
+# Tenants
 #
 
 class TenantGroupSerializer(ValidatedModelSerializer):
@@ -17,18 +17,6 @@ class TenantGroupSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedTenantGroupSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
-
-    class Meta:
-        model = TenantGroup
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Tenants
-#
-
 class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):
     group = NestedTenantGroupSerializer(required=False)
     tags = TagListSerializerField(required=False)
@@ -39,11 +27,3 @@ class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer):
             'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', 'created',
             'last_updated',
         ]
-
-
-class NestedTenantSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
-
-    class Meta:
-        model = Tenant
-        fields = ['id', 'url', 'name', 'slug']

+ 18 - 0
netbox/users/api/nested_serializers.py

@@ -0,0 +1,18 @@
+from django.contrib.auth.models import User
+
+from utilities.api import WritableNestedSerializer
+
+_all_ = [
+    'NestedUserSerializer',
+]
+
+
+#
+# Users
+#
+
+class NestedUserSerializer(WritableNestedSerializer):
+
+    class Meta:
+        model = User
+        fields = ['id', 'username']

+ 2 - 8
netbox/users/api/serializers.py

@@ -1,10 +1,4 @@
-from django.contrib.auth.models import User
+from .nested_serializers import *
 
-from utilities.api import WritableNestedSerializer
 
-
-class NestedUserSerializer(WritableNestedSerializer):
-
-    class Meta:
-        model = User
-        fields = ['id', 'username']
+# Placeholder for future serializers

+ 62 - 0
netbox/virtualization/api/nested_serializers.py

@@ -0,0 +1,62 @@
+from rest_framework import serializers
+
+from dcim.models import Interface
+from utilities.api import WritableNestedSerializer
+from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
+
+__all__ = [
+    'NestedClusterGroupSerializer',
+    'NestedClusterSerializer',
+    'NestedClusterTypeSerializer',
+    'NestedInterfaceSerializer',
+    'NestedVirtualMachineSerializer',
+]
+
+#
+# Clusters
+#
+
+
+class NestedClusterTypeSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
+
+    class Meta:
+        model = ClusterType
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedClusterGroupSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
+
+    class Meta:
+        model = ClusterGroup
+        fields = ['id', 'url', 'name', 'slug']
+
+
+class NestedClusterSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
+
+    class Meta:
+        model = Cluster
+        fields = ['id', 'url', 'name']
+
+
+#
+# Virtual machines
+#
+
+class NestedVirtualMachineSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
+
+    class Meta:
+        model = VirtualMachine
+        fields = ['id', 'url', 'name']
+
+
+class NestedInterfaceSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
+    virtual_machine = NestedVirtualMachineSerializer(read_only=True)
+
+    class Meta:
+        model = Interface
+        fields = ['id', 'url', 'virtual_machine', 'name']

+ 17 - 82
netbox/virtualization/api/serializers.py

@@ -1,19 +1,21 @@
 from rest_framework import serializers
 from taggit_serializer.serializers import TaggitSerializer, TagListSerializerField
 
-from dcim.api.serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
+from dcim.api.nested_serializers import NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer
 from dcim.constants import IFACE_FF_CHOICES, IFACE_FF_VIRTUAL, IFACE_MODE_CHOICES
 from dcim.models import Interface
 from extras.api.customfields import CustomFieldModelSerializer
-from ipam.models import IPAddress, VLAN
-from tenancy.api.serializers import NestedTenantSerializer
-from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer, WritableNestedSerializer
+from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
+from ipam.models import VLAN
+from tenancy.api.nested_serializers import NestedTenantSerializer
+from utilities.api import ChoiceField, SerializedPKRelatedField, ValidatedModelSerializer
 from virtualization.constants import VM_STATUS_CHOICES
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
+from .nested_serializers import *
 
 
 #
-# Cluster types
+# Clusters
 #
 
 class ClusterTypeSerializer(ValidatedModelSerializer):
@@ -23,18 +25,6 @@ class ClusterTypeSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedClusterTypeSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
-
-    class Meta:
-        model = ClusterType
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Cluster groups
-#
-
 class ClusterGroupSerializer(ValidatedModelSerializer):
 
     class Meta:
@@ -42,18 +32,6 @@ class ClusterGroupSerializer(ValidatedModelSerializer):
         fields = ['id', 'name', 'slug']
 
 
-class NestedClusterGroupSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
-
-    class Meta:
-        model = ClusterGroup
-        fields = ['id', 'url', 'name', 'slug']
-
-
-#
-# Clusters
-#
-
 class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
     type = NestedClusterTypeSerializer()
     group = NestedClusterGroupSerializer(required=False, allow_null=True)
@@ -67,27 +45,10 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer):
         ]
 
 
-class NestedClusterSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
-
-    class Meta:
-        model = Cluster
-        fields = ['id', 'url', 'name']
-
-
 #
 # Virtual machines
 #
 
-# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency
-class VirtualMachineIPAddressSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
-
-    class Meta:
-        model = IPAddress
-        fields = ['id', 'url', 'family', 'address']
-
-
 class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
     status = ChoiceField(choices=VM_STATUS_CHOICES, required=False)
     site = NestedSiteSerializer(read_only=True)
@@ -95,17 +56,17 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer):
     role = NestedDeviceRoleSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     platform = NestedPlatformSerializer(required=False, allow_null=True)
-    primary_ip = VirtualMachineIPAddressSerializer(read_only=True)
-    primary_ip4 = VirtualMachineIPAddressSerializer(required=False, allow_null=True)
-    primary_ip6 = VirtualMachineIPAddressSerializer(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)
     tags = TagListSerializerField(required=False)
 
     class Meta:
         model = VirtualMachine
         fields = [
             'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4',
-            'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-            'local_context_data',
+            'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', 'custom_fields',
+            'created', 'last_updated',
         ]
 
 
@@ -114,44 +75,27 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
 
     class Meta(VirtualMachineSerializer.Meta):
         fields = [
-            'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6',
-            'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
-            'local_context_data',
+            'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4',
+            'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'local_context_data', 'tags', 'custom_fields',
+            'config_context', 'created', 'last_updated',
         ]
 
     def get_config_context(self, obj):
         return obj.get_config_context()
 
 
-class NestedVirtualMachineSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
-
-    class Meta:
-        model = VirtualMachine
-        fields = ['id', 'url', 'name']
-
-
 #
 # VM interfaces
 #
 
-# Cannot import ipam.api.serializers.NestedVLANSerializer due to circular dependency
-class InterfaceVLANSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
-
-    class Meta:
-        model = VLAN
-        fields = ['id', 'url', 'vid', 'name', 'display_name']
-
-
 class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
     virtual_machine = NestedVirtualMachineSerializer()
     form_factor = ChoiceField(choices=IFACE_FF_CHOICES, default=IFACE_FF_VIRTUAL, required=False)
     mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
-    untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
+    untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
     tagged_vlans = SerializedPKRelatedField(
         queryset=VLAN.objects.all(),
-        serializer=InterfaceVLANSerializer,
+        serializer=NestedVLANSerializer,
         required=False,
         many=True
     )
@@ -163,12 +107,3 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
             'id', 'virtual_machine', 'name', 'form_factor', 'enabled', 'mtu', 'mac_address', 'description', 'mode',
             'untagged_vlan', 'tagged_vlans', 'tags',
         ]
-
-
-class NestedInterfaceSerializer(WritableNestedSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:interface-detail')
-    virtual_machine = NestedVirtualMachineSerializer(read_only=True)
-
-    class Meta:
-        model = Interface
-        fields = ['id', 'url', 'virtual_machine', 'name']