Parcourir la source

Extended API to include custom fields

Jeremy Stretch il y a 8 ans
Parent
commit
76f0463290

+ 6 - 4
netbox/circuits/api/serializers.py

@@ -2,6 +2,7 @@ from rest_framework import serializers
 
 from circuits.models import Provider, CircuitType, Circuit
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
+from extras.api.serializers import CustomFieldsSerializer
 from tenancy.api.serializers import TenantNestedSerializer
 
 
@@ -9,11 +10,12 @@ from tenancy.api.serializers import TenantNestedSerializer
 # Providers
 #
 
-class ProviderSerializer(serializers.ModelSerializer):
+class ProviderSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
 
     class Meta:
         model = Provider
-        fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments']
+        fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments',
+                  'custom_fields']
 
 
 class ProviderNestedSerializer(ProviderSerializer):
@@ -43,7 +45,7 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
 # Circuits
 #
 
-class CircuitSerializer(serializers.ModelSerializer):
+class CircuitSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     provider = ProviderNestedSerializer()
     type = CircuitTypeNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -53,7 +55,7 @@ class CircuitSerializer(serializers.ModelSerializer):
     class Meta:
         model = Circuit
         fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
-                  'upstream_speed', 'commit_rate', 'xconnect_id', 'comments']
+                  'upstream_speed', 'commit_rate', 'xconnect_id', 'comments', 'custom_fields']
 
 
 class CircuitNestedSerializer(CircuitSerializer):

+ 11 - 8
netbox/circuits/api/views.py

@@ -3,22 +3,23 @@ from rest_framework import generics
 from circuits.models import Provider, CircuitType, Circuit
 from circuits.filters import CircuitFilter
 
+from extras.api.views import CustomFieldModelAPIView
 from . import serializers
 
 
-class ProviderListView(generics.ListAPIView):
+class ProviderListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List all providers
     """
-    queryset = Provider.objects.all()
+    queryset = Provider.objects.prefetch_related('custom_field_values')
     serializer_class = serializers.ProviderSerializer
 
 
-class ProviderDetailView(generics.RetrieveAPIView):
+class ProviderDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single provider
     """
-    queryset = Provider.objects.all()
+    queryset = Provider.objects.prefetch_related('custom_field_values')
     serializer_class = serializers.ProviderSerializer
 
 
@@ -38,18 +39,20 @@ class CircuitTypeDetailView(generics.RetrieveAPIView):
     serializer_class = serializers.CircuitTypeSerializer
 
 
-class CircuitListView(generics.ListAPIView):
+class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List circuits (filterable)
     """
-    queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
+    queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\
+        .prefetch_related('custom_field_values')
     serializer_class = serializers.CircuitSerializer
     filter_class = CircuitFilter
 
 
-class CircuitDetailView(generics.RetrieveAPIView):
+class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single circuit
     """
-    queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')
+    queryset = Circuit.objects.select_related('type', 'tenant', 'provider', 'site', 'interface__device')\
+        .prefetch_related('custom_field_values')
     serializer_class = serializers.CircuitSerializer

+ 4 - 1
netbox/circuits/models.py

@@ -1,9 +1,10 @@
+from django.contrib.contenttypes.fields import GenericRelation
 from django.core.urlresolvers import reverse
 from django.db import models
 
 from dcim.fields import ASNField
 from dcim.models import Site, Interface
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, CustomFieldValue
 from tenancy.models import Tenant
 from utilities.models import CreatedUpdatedModel
 
@@ -21,6 +22,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
     noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
     admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     class Meta:
         ordering = ['name']
@@ -79,6 +81,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
     xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
     pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     class Meta:
         ordering = ['provider', 'cid']

+ 7 - 6
netbox/dcim/api/serializers.py

@@ -6,6 +6,7 @@ from dcim.models import (
     DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
     PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
 )
+from extras.api.serializers import CustomFieldsSerializer
 from tenancy.api.serializers import TenantNestedSerializer
 
 
@@ -13,13 +14,13 @@ from tenancy.api.serializers import TenantNestedSerializer
 # Sites
 #
 
-class SiteSerializer(serializers.ModelSerializer):
+class SiteSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     tenant = TenantNestedSerializer()
 
     class Meta:
         model = Site
         fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
-                  'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
+                  'custom_fields', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits']
 
 
 class SiteNestedSerializer(SiteSerializer):
@@ -68,7 +69,7 @@ class RackRoleNestedSerializer(RackRoleSerializer):
 #
 
 
-class RackSerializer(serializers.ModelSerializer):
+class RackSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     site = SiteNestedSerializer()
     group = RackGroupNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -77,7 +78,7 @@ class RackSerializer(serializers.ModelSerializer):
     class Meta:
         model = Rack
         fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
-                  'u_height', 'comments']
+                  'u_height', 'comments', 'custom_fields']
 
 
 class RackNestedSerializer(RackSerializer):
@@ -237,7 +238,7 @@ class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
         fields = ['id', 'family', 'address']
 
 
-class DeviceSerializer(serializers.ModelSerializer):
+class DeviceSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     device_type = DeviceTypeNestedSerializer()
     device_role = DeviceRoleNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -252,7 +253,7 @@ class DeviceSerializer(serializers.ModelSerializer):
         model = Device
         fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
                   'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
-                  'primary_ip6', 'comments']
+                  'primary_ip6', 'comments', 'custom_fields']
 
     def get_parent_device(self, obj):
         try:

+ 16 - 14
netbox/dcim/api/views.py

@@ -13,29 +13,30 @@ from dcim.models import (
     InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
 )
 from dcim import filters
-from .exceptions import MissingFilterException
-from . import serializers
+from extras.api.views import CustomFieldModelAPIView
 from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
 from utilities.api import ServiceUnavailable
+from .exceptions import MissingFilterException
+from . import serializers
 
 
 #
 # Sites
 #
 
-class SiteListView(generics.ListAPIView):
+class SiteListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List all sites
     """
-    queryset = Site.objects.select_related('tenant')
+    queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values')
     serializer_class = serializers.SiteSerializer
 
 
-class SiteDetailView(generics.RetrieveAPIView):
+class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single site
     """
-    queryset = Site.objects.select_related('tenant')
+    queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values')
     serializer_class = serializers.SiteSerializer
 
 
@@ -84,20 +85,20 @@ class RackRoleDetailView(generics.RetrieveAPIView):
 # Racks
 #
 
-class RackListView(generics.ListAPIView):
+class RackListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List racks (filterable)
     """
-    queryset = Rack.objects.select_related('site', 'group', 'tenant')
+    queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('custom_field_values')
     serializer_class = serializers.RackSerializer
     filter_class = filters.RackFilter
 
 
-class RackDetailView(generics.RetrieveAPIView):
+class RackDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single rack
     """
-    queryset = Rack.objects.select_related('site', 'group', 'tenant')
+    queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('custom_field_values')
     serializer_class = serializers.RackDetailSerializer
 
 
@@ -209,24 +210,25 @@ class PlatformDetailView(generics.RetrieveAPIView):
 # Devices
 #
 
-class DeviceListView(generics.ListAPIView):
+class DeviceListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List devices (filterable)
     """
     queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
                                              'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
-                                                                                          'primary_ip6__nat_outside')
+                                                                                          'primary_ip6__nat_outside',
+                                                                                          'custom_field_values')
     serializer_class = serializers.DeviceSerializer
     filter_class = filters.DeviceFilter
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
 
 
-class DeviceDetailView(generics.RetrieveAPIView):
+class DeviceDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single device
     """
     queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
-                                             'rack__site', 'parent_bay')
+                                             'rack__site', 'parent_bay').prefetch_related('custom_field_values')
     serializer_class = serializers.DeviceSerializer
 
 

+ 4 - 0
netbox/dcim/models.py

@@ -2,6 +2,7 @@ from collections import OrderedDict
 
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import MultipleObjectsReturned, ValidationError
 from django.core.urlresolvers import reverse
 from django.core.validators import MaxValueValidator, MinValueValidator
@@ -228,6 +229,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
     physical_address = models.CharField(max_length=200, blank=True)
     shipping_address = models.CharField(max_length=200, blank=True)
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     objects = SiteManager()
 
@@ -339,6 +341,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
     u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
                                                 validators=[MinValueValidator(1), MaxValueValidator(100)])
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     objects = RackManager()
 
@@ -752,6 +755,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
     primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL,
                                        blank=True, null=True, verbose_name='Primary IPv6')
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     objects = DeviceManager()
 

+ 24 - 1
netbox/extras/api/serializers.py

@@ -1,6 +1,29 @@
 from rest_framework import serializers
 
-from extras.models import Graph
+from extras.models import CF_TYPE_SELECT, CustomFieldChoice, Graph
+
+
+class CustomFieldsSerializer(serializers.Serializer):
+    """
+    Extends a ModelSerializer to render any CustomFields and their values associated with an object.
+    """
+    custom_fields = serializers.SerializerMethodField()
+
+    def get_custom_fields(self, obj):
+        fields = {cf.name: None for cf in self.context['view'].custom_fields}
+        for cfv in obj.custom_field_values.all():
+            if cfv.field.type == CF_TYPE_SELECT:
+                fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfv.value).data
+            else:
+                fields[cfv.field.name] = cfv.value
+        return fields
+
+
+class CustomFieldChoiceSerializer(serializers.ModelSerializer):
+
+    class Meta:
+        model = CustomFieldChoice
+        fields = ['id', 'value']
 
 
 class GraphSerializer(serializers.ModelSerializer):

+ 12 - 2
netbox/extras/api/views.py

@@ -1,9 +1,8 @@
 import graphviz
 from rest_framework import generics
 from rest_framework.views import APIView
-import tempfile
-from wsgiref.util import FileWrapper
 
+from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
@@ -15,6 +14,17 @@ from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_P
 from .serializers import GraphSerializer
 
 
+class CustomFieldModelAPIView(object):
+    """
+    Include the applicable set of CustomField in the view context.
+    """
+
+    def __init__(self):
+        super(CustomFieldModelAPIView, self).__init__()
+        self.content_type = ContentType.objects.get_for_model(self.queryset.model)
+        self.custom_fields = self.content_type.custom_fields.all()
+
+
 class GraphListView(generics.ListAPIView):
     """
     Returns a list of relevant graphs

+ 14 - 10
netbox/ipam/api/serializers.py

@@ -1,6 +1,7 @@
 from rest_framework import serializers
 
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
+from extras.api.serializers import CustomFieldsSerializer
 from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
 from tenancy.api.serializers import TenantNestedSerializer
 
@@ -9,12 +10,12 @@ from tenancy.api.serializers import TenantNestedSerializer
 # VRFs
 #
 
-class VRFSerializer(serializers.ModelSerializer):
+class VRFSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     tenant = TenantNestedSerializer()
 
     class Meta:
         model = VRF
-        fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description']
+        fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields']
 
 
 class VRFNestedSerializer(VRFSerializer):
@@ -70,12 +71,12 @@ class RIRNestedSerializer(RIRSerializer):
 # Aggregates
 #
 
-class AggregateSerializer(serializers.ModelSerializer):
+class AggregateSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     rir = RIRNestedSerializer()
 
     class Meta:
         model = Aggregate
-        fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description']
+        fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields']
 
 
 class AggregateNestedSerializer(AggregateSerializer):
@@ -106,7 +107,7 @@ class VLANGroupNestedSerializer(VLANGroupSerializer):
 # VLANs
 #
 
-class VLANSerializer(serializers.ModelSerializer):
+class VLANSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     site = SiteNestedSerializer()
     group = VLANGroupNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -114,7 +115,8 @@ class VLANSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = VLAN
-        fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name']
+        fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name',
+                  'custom_fields']
 
 
 class VLANNestedSerializer(VLANSerializer):
@@ -127,7 +129,7 @@ class VLANNestedSerializer(VLANSerializer):
 # Prefixes
 #
 
-class PrefixSerializer(serializers.ModelSerializer):
+class PrefixSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     site = SiteNestedSerializer()
     vrf = VRFTenantSerializer()
     tenant = TenantNestedSerializer()
@@ -136,7 +138,8 @@ class PrefixSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Prefix
-        fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description']
+        fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description',
+                  'custom_fields']
 
 
 class PrefixNestedSerializer(PrefixSerializer):
@@ -149,14 +152,15 @@ class PrefixNestedSerializer(PrefixSerializer):
 # IP addresses
 #
 
-class IPAddressSerializer(serializers.ModelSerializer):
+class IPAddressSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     vrf = VRFTenantSerializer()
     tenant = TenantNestedSerializer()
     interface = InterfaceNestedSerializer()
 
     class Meta:
         model = IPAddress
-        fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside']
+        fields = ['id', 'family', 'address', 'vrf', 'tenant', 'interface', 'description', 'nat_inside', 'nat_outside',
+                  'custom_fields']
 
 
 class IPAddressNestedSerializer(IPAddressSerializer):

+ 23 - 20
netbox/ipam/api/views.py

@@ -3,6 +3,7 @@ from rest_framework import generics
 from ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
 from ipam import filters
 
+from extras.api.views import CustomFieldModelAPIView
 from . import serializers
 
 
@@ -10,20 +11,20 @@ from . import serializers
 # VRFs
 #
 
-class VRFListView(generics.ListAPIView):
+class VRFListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List all VRFs
     """
-    queryset = VRF.objects.select_related('tenant')
+    queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values')
     serializer_class = serializers.VRFSerializer
     filter_class = filters.VRFFilter
 
 
-class VRFDetailView(generics.RetrieveAPIView):
+class VRFDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single VRF
     """
-    queryset = VRF.objects.select_related('tenant')
+    queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values')
     serializer_class = serializers.VRFSerializer
 
 
@@ -71,20 +72,20 @@ class RIRDetailView(generics.RetrieveAPIView):
 # Aggregates
 #
 
-class AggregateListView(generics.ListAPIView):
+class AggregateListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List aggregates (filterable)
     """
-    queryset = Aggregate.objects.select_related('rir')
+    queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values')
     serializer_class = serializers.AggregateSerializer
     filter_class = filters.AggregateFilter
 
 
-class AggregateDetailView(generics.RetrieveAPIView):
+class AggregateDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single aggregate
     """
-    queryset = Aggregate.objects.select_related('rir')
+    queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values')
     serializer_class = serializers.AggregateSerializer
 
 
@@ -92,20 +93,22 @@ class AggregateDetailView(generics.RetrieveAPIView):
 # Prefixes
 #
 
-class PrefixListView(generics.ListAPIView):
+class PrefixListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List prefixes (filterable)
     """
-    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
+    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')\
+        .prefetch_related('custom_field_values')
     serializer_class = serializers.PrefixSerializer
     filter_class = filters.PrefixFilter
 
 
-class PrefixDetailView(generics.RetrieveAPIView):
+class PrefixDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single prefix
     """
-    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
+    queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')\
+        .prefetch_related('custom_field_values')
     serializer_class = serializers.PrefixSerializer
 
 
@@ -113,22 +116,22 @@ class PrefixDetailView(generics.RetrieveAPIView):
 # IP addresses
 #
 
-class IPAddressListView(generics.ListAPIView):
+class IPAddressListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List IP addresses (filterable)
     """
     queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
-        .prefetch_related('nat_outside')
+        .prefetch_related('nat_outside', 'custom_field_values')
     serializer_class = serializers.IPAddressSerializer
     filter_class = filters.IPAddressFilter
 
 
-class IPAddressDetailView(generics.RetrieveAPIView):
+class IPAddressDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single IP address
     """
     queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
-        .prefetch_related('nat_outside')
+        .prefetch_related('nat_outside', 'custom_field_values')
     serializer_class = serializers.IPAddressSerializer
 
 
@@ -157,18 +160,18 @@ class VLANGroupDetailView(generics.RetrieveAPIView):
 # VLANs
 #
 
-class VLANListView(generics.ListAPIView):
+class VLANListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List VLANs (filterable)
     """
-    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
+    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('custom_field_values')
     serializer_class = serializers.VLANSerializer
     filter_class = filters.VLANFilter
 
 
-class VLANDetailView(generics.RetrieveAPIView):
+class VLANDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single VLAN
     """
-    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')
+    queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('custom_field_values')
     serializer_class = serializers.VLANSerializer

+ 7 - 1
netbox/ipam/models.py

@@ -1,6 +1,7 @@
 from netaddr import IPNetwork, cidr_merge
 
 from django.conf import settings
+from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.core.validators import MaxValueValidator, MinValueValidator
@@ -8,7 +9,7 @@ from django.db import models
 from django.db.models.expressions import RawSQL
 
 from dcim.models import Interface
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, CustomFieldValue
 from tenancy.models import Tenant
 from utilities.models import CreatedUpdatedModel
 
@@ -53,6 +54,7 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
     enforce_unique = models.BooleanField(default=True, verbose_name='Enforce unique space',
                                          help_text="Prevent duplicate prefixes/IP addresses within this VRF")
     description = models.CharField(max_length=100, blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     class Meta:
         ordering = ['name']
@@ -105,6 +107,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
     rir = models.ForeignKey('RIR', related_name='aggregates', on_delete=models.PROTECT, verbose_name='RIR')
     date_added = models.DateField(blank=True, null=True)
     description = models.CharField(max_length=100, blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     class Meta:
         ordering = ['family', 'prefix']
@@ -241,6 +244,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
     status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
     role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True)
     description = models.CharField(max_length=100, blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     objects = PrefixQuerySet.as_manager()
 
@@ -332,6 +336,7 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
                                       null=True, verbose_name='NAT IP (inside)')
     description = models.CharField(max_length=100, blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     objects = IPAddressManager()
 
@@ -436,6 +441,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
     status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
     role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
     description = models.CharField(max_length=100, blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     class Meta:
         ordering = ['site', 'group', 'vid']

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

@@ -1,5 +1,6 @@
 from rest_framework import serializers
 
+from extras.api.serializers import CustomFieldsSerializer
 from tenancy.models import Tenant, TenantGroup
 
 
@@ -24,12 +25,12 @@ class TenantGroupNestedSerializer(TenantGroupSerializer):
 # Tenants
 #
 
-class TenantSerializer(serializers.ModelSerializer):
+class TenantSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     group = TenantGroupNestedSerializer()
 
     class Meta:
         model = Tenant
-        fields = ['id', 'name', 'slug', 'group', 'comments']
+        fields = ['id', 'name', 'slug', 'group', 'comments', 'custom_fields']
 
 
 class TenantNestedSerializer(TenantSerializer):

+ 5 - 4
netbox/tenancy/api/views.py

@@ -3,6 +3,7 @@ from rest_framework import generics
 from tenancy.models import Tenant, TenantGroup
 from tenancy.filters import TenantFilter
 
+from extras.api.views import CustomFieldModelAPIView
 from . import serializers
 
 
@@ -22,18 +23,18 @@ class TenantGroupDetailView(generics.RetrieveAPIView):
     serializer_class = serializers.TenantGroupSerializer
 
 
-class TenantListView(generics.ListAPIView):
+class TenantListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     List tenants (filterable)
     """
-    queryset = Tenant.objects.select_related('group')
+    queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values')
     serializer_class = serializers.TenantSerializer
     filter_class = TenantFilter
 
 
-class TenantDetailView(generics.RetrieveAPIView):
+class TenantDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     Retrieve a single tenant
     """
-    queryset = Tenant.objects.select_related('group')
+    queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values')
     serializer_class = serializers.TenantSerializer

+ 3 - 1
netbox/tenancy/models.py

@@ -1,7 +1,8 @@
+from django.contrib.contenttypes.fields import GenericRelation
 from django.core.urlresolvers import reverse
 from django.db import models
 
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, CustomFieldValue
 from utilities.models import CreatedUpdatedModel
 
 
@@ -32,6 +33,7 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
     group = models.ForeignKey('TenantGroup', related_name='tenants', blank=True, null=True, on_delete=models.SET_NULL)
     description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)")
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
     class Meta:
         ordering = ['group', 'name']