Browse Source

Extended API to include custom fields

Jeremy Stretch 8 years ago
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 circuits.models import Provider, CircuitType, Circuit
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
+from extras.api.serializers import CustomFieldsSerializer
 from tenancy.api.serializers import TenantNestedSerializer
 from tenancy.api.serializers import TenantNestedSerializer
 
 
 
 
@@ -9,11 +10,12 @@ from tenancy.api.serializers import TenantNestedSerializer
 # Providers
 # Providers
 #
 #
 
 
-class ProviderSerializer(serializers.ModelSerializer):
+class ProviderSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Provider
         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):
 class ProviderNestedSerializer(ProviderSerializer):
@@ -43,7 +45,7 @@ class CircuitTypeNestedSerializer(CircuitTypeSerializer):
 # Circuits
 # Circuits
 #
 #
 
 
-class CircuitSerializer(serializers.ModelSerializer):
+class CircuitSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     provider = ProviderNestedSerializer()
     provider = ProviderNestedSerializer()
     type = CircuitTypeNestedSerializer()
     type = CircuitTypeNestedSerializer()
     tenant = TenantNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -53,7 +55,7 @@ class CircuitSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
         fields = ['id', 'cid', 'provider', 'type', 'tenant', 'site', 'interface', 'install_date', 'port_speed',
         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):
 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.models import Provider, CircuitType, Circuit
 from circuits.filters import CircuitFilter
 from circuits.filters import CircuitFilter
 
 
+from extras.api.views import CustomFieldModelAPIView
 from . import serializers
 from . import serializers
 
 
 
 
-class ProviderListView(generics.ListAPIView):
+class ProviderListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List all providers
     List all providers
     """
     """
-    queryset = Provider.objects.all()
+    queryset = Provider.objects.prefetch_related('custom_field_values')
     serializer_class = serializers.ProviderSerializer
     serializer_class = serializers.ProviderSerializer
 
 
 
 
-class ProviderDetailView(generics.RetrieveAPIView):
+class ProviderDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single provider
     Retrieve a single provider
     """
     """
-    queryset = Provider.objects.all()
+    queryset = Provider.objects.prefetch_related('custom_field_values')
     serializer_class = serializers.ProviderSerializer
     serializer_class = serializers.ProviderSerializer
 
 
 
 
@@ -38,18 +39,20 @@ class CircuitTypeDetailView(generics.RetrieveAPIView):
     serializer_class = serializers.CircuitTypeSerializer
     serializer_class = serializers.CircuitTypeSerializer
 
 
 
 
-class CircuitListView(generics.ListAPIView):
+class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List circuits (filterable)
     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
     serializer_class = serializers.CircuitSerializer
     filter_class = CircuitFilter
     filter_class = CircuitFilter
 
 
 
 
-class CircuitDetailView(generics.RetrieveAPIView):
+class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single circuit
     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
     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.core.urlresolvers import reverse
 from django.db import models
 from django.db import models
 
 
 from dcim.fields import ASNField
 from dcim.fields import ASNField
 from dcim.models import Site, Interface
 from dcim.models import Site, Interface
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, CustomFieldValue
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
 
 
@@ -21,6 +22,7 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
     noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
     noc_contact = models.TextField(blank=True, verbose_name='NOC contact')
     admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
     admin_contact = models.TextField(blank=True, verbose_name='Admin contact')
     comments = models.TextField(blank=True)
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
 
     class Meta:
     class Meta:
         ordering = ['name']
         ordering = ['name']
@@ -79,6 +81,7 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
     xconnect_id = models.CharField(max_length=50, blank=True, verbose_name='Cross-connect ID')
     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)')
     pp_info = models.CharField(max_length=100, blank=True, verbose_name='Patch panel/port(s)')
     comments = models.TextField(blank=True)
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
 
     class Meta:
     class Meta:
         ordering = ['provider', 'cid']
         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,
     DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet,
     PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site,
     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
 from tenancy.api.serializers import TenantNestedSerializer
 
 
 
 
@@ -13,13 +14,13 @@ from tenancy.api.serializers import TenantNestedSerializer
 # Sites
 # Sites
 #
 #
 
 
-class SiteSerializer(serializers.ModelSerializer):
+class SiteSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     tenant = TenantNestedSerializer()
     tenant = TenantNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = Site
         model = Site
         fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'comments',
         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):
 class SiteNestedSerializer(SiteSerializer):
@@ -68,7 +69,7 @@ class RackRoleNestedSerializer(RackRoleSerializer):
 #
 #
 
 
 
 
-class RackSerializer(serializers.ModelSerializer):
+class RackSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     site = SiteNestedSerializer()
     site = SiteNestedSerializer()
     group = RackGroupNestedSerializer()
     group = RackGroupNestedSerializer()
     tenant = TenantNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -77,7 +78,7 @@ class RackSerializer(serializers.ModelSerializer):
     class Meta:
     class Meta:
         model = Rack
         model = Rack
         fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
         fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width',
-                  'u_height', 'comments']
+                  'u_height', 'comments', 'custom_fields']
 
 
 
 
 class RackNestedSerializer(RackSerializer):
 class RackNestedSerializer(RackSerializer):
@@ -237,7 +238,7 @@ class DeviceIPAddressNestedSerializer(serializers.ModelSerializer):
         fields = ['id', 'family', 'address']
         fields = ['id', 'family', 'address']
 
 
 
 
-class DeviceSerializer(serializers.ModelSerializer):
+class DeviceSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     device_type = DeviceTypeNestedSerializer()
     device_type = DeviceTypeNestedSerializer()
     device_role = DeviceRoleNestedSerializer()
     device_role = DeviceRoleNestedSerializer()
     tenant = TenantNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -252,7 +253,7 @@ class DeviceSerializer(serializers.ModelSerializer):
         model = Device
         model = Device
         fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
         fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial',
                   'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4',
                   '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):
     def get_parent_device(self, obj):
         try:
         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,
     InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site,
 )
 )
 from dcim import filters
 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 extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer
 from utilities.api import ServiceUnavailable
 from utilities.api import ServiceUnavailable
+from .exceptions import MissingFilterException
+from . import serializers
 
 
 
 
 #
 #
 # Sites
 # Sites
 #
 #
 
 
-class SiteListView(generics.ListAPIView):
+class SiteListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List all sites
     List all sites
     """
     """
-    queryset = Site.objects.select_related('tenant')
+    queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values')
     serializer_class = serializers.SiteSerializer
     serializer_class = serializers.SiteSerializer
 
 
 
 
-class SiteDetailView(generics.RetrieveAPIView):
+class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single site
     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
     serializer_class = serializers.SiteSerializer
 
 
 
 
@@ -84,20 +85,20 @@ class RackRoleDetailView(generics.RetrieveAPIView):
 # Racks
 # Racks
 #
 #
 
 
-class RackListView(generics.ListAPIView):
+class RackListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List racks (filterable)
     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
     serializer_class = serializers.RackSerializer
     filter_class = filters.RackFilter
     filter_class = filters.RackFilter
 
 
 
 
-class RackDetailView(generics.RetrieveAPIView):
+class RackDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single rack
     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
     serializer_class = serializers.RackDetailSerializer
 
 
 
 
@@ -209,24 +210,25 @@ class PlatformDetailView(generics.RetrieveAPIView):
 # Devices
 # Devices
 #
 #
 
 
-class DeviceListView(generics.ListAPIView):
+class DeviceListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List devices (filterable)
     List devices (filterable)
     """
     """
     queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
     queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
                                              'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside',
                                              '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
     serializer_class = serializers.DeviceSerializer
     filter_class = filters.DeviceFilter
     filter_class = filters.DeviceFilter
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]
 
 
 
 
-class DeviceDetailView(generics.RetrieveAPIView):
+class DeviceDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single device
     Retrieve a single device
     """
     """
     queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform',
     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
     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.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.fields import GenericRelation
 from django.core.exceptions import MultipleObjectsReturned, ValidationError
 from django.core.exceptions import MultipleObjectsReturned, ValidationError
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
@@ -228,6 +229,7 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
     physical_address = models.CharField(max_length=200, blank=True)
     physical_address = models.CharField(max_length=200, blank=True)
     shipping_address = models.CharField(max_length=200, blank=True)
     shipping_address = models.CharField(max_length=200, blank=True)
     comments = models.TextField(blank=True)
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
 
     objects = SiteManager()
     objects = SiteManager()
 
 
@@ -339,6 +341,7 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
     u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
     u_height = models.PositiveSmallIntegerField(default=42, verbose_name='Height (U)',
                                                 validators=[MinValueValidator(1), MaxValueValidator(100)])
                                                 validators=[MinValueValidator(1), MaxValueValidator(100)])
     comments = models.TextField(blank=True)
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
 
     objects = RackManager()
     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,
     primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL,
                                        blank=True, null=True, verbose_name='Primary IPv6')
                                        blank=True, null=True, verbose_name='Primary IPv6')
     comments = models.TextField(blank=True)
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
 
     objects = DeviceManager()
     objects = DeviceManager()
 
 

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

@@ -1,6 +1,29 @@
 from rest_framework import serializers
 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):
 class GraphSerializer(serializers.ModelSerializer):

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

@@ -1,9 +1,8 @@
 import graphviz
 import graphviz
 from rest_framework import generics
 from rest_framework import generics
 from rest_framework.views import APIView
 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.db.models import Q
 from django.http import Http404, HttpResponse
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
 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
 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):
 class GraphListView(generics.ListAPIView):
     """
     """
     Returns a list of relevant graphs
     Returns a list of relevant graphs

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

@@ -1,6 +1,7 @@
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer
 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 ipam.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
 from tenancy.api.serializers import TenantNestedSerializer
 from tenancy.api.serializers import TenantNestedSerializer
 
 
@@ -9,12 +10,12 @@ from tenancy.api.serializers import TenantNestedSerializer
 # VRFs
 # VRFs
 #
 #
 
 
-class VRFSerializer(serializers.ModelSerializer):
+class VRFSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     tenant = TenantNestedSerializer()
     tenant = TenantNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = VRF
         model = VRF
-        fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description']
+        fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields']
 
 
 
 
 class VRFNestedSerializer(VRFSerializer):
 class VRFNestedSerializer(VRFSerializer):
@@ -70,12 +71,12 @@ class RIRNestedSerializer(RIRSerializer):
 # Aggregates
 # Aggregates
 #
 #
 
 
-class AggregateSerializer(serializers.ModelSerializer):
+class AggregateSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     rir = RIRNestedSerializer()
     rir = RIRNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = Aggregate
         model = Aggregate
-        fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description']
+        fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields']
 
 
 
 
 class AggregateNestedSerializer(AggregateSerializer):
 class AggregateNestedSerializer(AggregateSerializer):
@@ -106,7 +107,7 @@ class VLANGroupNestedSerializer(VLANGroupSerializer):
 # VLANs
 # VLANs
 #
 #
 
 
-class VLANSerializer(serializers.ModelSerializer):
+class VLANSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     site = SiteNestedSerializer()
     site = SiteNestedSerializer()
     group = VLANGroupNestedSerializer()
     group = VLANGroupNestedSerializer()
     tenant = TenantNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -114,7 +115,8 @@ class VLANSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = VLAN
         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):
 class VLANNestedSerializer(VLANSerializer):
@@ -127,7 +129,7 @@ class VLANNestedSerializer(VLANSerializer):
 # Prefixes
 # Prefixes
 #
 #
 
 
-class PrefixSerializer(serializers.ModelSerializer):
+class PrefixSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     site = SiteNestedSerializer()
     site = SiteNestedSerializer()
     vrf = VRFTenantSerializer()
     vrf = VRFTenantSerializer()
     tenant = TenantNestedSerializer()
     tenant = TenantNestedSerializer()
@@ -136,7 +138,8 @@ class PrefixSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Prefix
         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):
 class PrefixNestedSerializer(PrefixSerializer):
@@ -149,14 +152,15 @@ class PrefixNestedSerializer(PrefixSerializer):
 # IP addresses
 # IP addresses
 #
 #
 
 
-class IPAddressSerializer(serializers.ModelSerializer):
+class IPAddressSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     vrf = VRFTenantSerializer()
     vrf = VRFTenantSerializer()
     tenant = TenantNestedSerializer()
     tenant = TenantNestedSerializer()
     interface = InterfaceNestedSerializer()
     interface = InterfaceNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = IPAddress
         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):
 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.models import VRF, Role, RIR, Aggregate, Prefix, IPAddress, VLAN, VLANGroup
 from ipam import filters
 from ipam import filters
 
 
+from extras.api.views import CustomFieldModelAPIView
 from . import serializers
 from . import serializers
 
 
 
 
@@ -10,20 +11,20 @@ from . import serializers
 # VRFs
 # VRFs
 #
 #
 
 
-class VRFListView(generics.ListAPIView):
+class VRFListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List all VRFs
     List all VRFs
     """
     """
-    queryset = VRF.objects.select_related('tenant')
+    queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values')
     serializer_class = serializers.VRFSerializer
     serializer_class = serializers.VRFSerializer
     filter_class = filters.VRFFilter
     filter_class = filters.VRFFilter
 
 
 
 
-class VRFDetailView(generics.RetrieveAPIView):
+class VRFDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single VRF
     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
     serializer_class = serializers.VRFSerializer
 
 
 
 
@@ -71,20 +72,20 @@ class RIRDetailView(generics.RetrieveAPIView):
 # Aggregates
 # Aggregates
 #
 #
 
 
-class AggregateListView(generics.ListAPIView):
+class AggregateListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List aggregates (filterable)
     List aggregates (filterable)
     """
     """
-    queryset = Aggregate.objects.select_related('rir')
+    queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values')
     serializer_class = serializers.AggregateSerializer
     serializer_class = serializers.AggregateSerializer
     filter_class = filters.AggregateFilter
     filter_class = filters.AggregateFilter
 
 
 
 
-class AggregateDetailView(generics.RetrieveAPIView):
+class AggregateDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single aggregate
     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
     serializer_class = serializers.AggregateSerializer
 
 
 
 
@@ -92,20 +93,22 @@ class AggregateDetailView(generics.RetrieveAPIView):
 # Prefixes
 # Prefixes
 #
 #
 
 
-class PrefixListView(generics.ListAPIView):
+class PrefixListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List prefixes (filterable)
     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
     serializer_class = serializers.PrefixSerializer
     filter_class = filters.PrefixFilter
     filter_class = filters.PrefixFilter
 
 
 
 
-class PrefixDetailView(generics.RetrieveAPIView):
+class PrefixDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single prefix
     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
     serializer_class = serializers.PrefixSerializer
 
 
 
 
@@ -113,22 +116,22 @@ class PrefixDetailView(generics.RetrieveAPIView):
 # IP addresses
 # IP addresses
 #
 #
 
 
-class IPAddressListView(generics.ListAPIView):
+class IPAddressListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List IP addresses (filterable)
     List IP addresses (filterable)
     """
     """
     queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
     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
     serializer_class = serializers.IPAddressSerializer
     filter_class = filters.IPAddressFilter
     filter_class = filters.IPAddressFilter
 
 
 
 
-class IPAddressDetailView(generics.RetrieveAPIView):
+class IPAddressDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single IP address
     Retrieve a single IP address
     """
     """
     queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\
     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
     serializer_class = serializers.IPAddressSerializer
 
 
 
 
@@ -157,18 +160,18 @@ class VLANGroupDetailView(generics.RetrieveAPIView):
 # VLANs
 # VLANs
 #
 #
 
 
-class VLANListView(generics.ListAPIView):
+class VLANListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List VLANs (filterable)
     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
     serializer_class = serializers.VLANSerializer
     filter_class = filters.VLANFilter
     filter_class = filters.VLANFilter
 
 
 
 
-class VLANDetailView(generics.RetrieveAPIView):
+class VLANDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single VLAN
     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
     serializer_class = serializers.VLANSerializer

+ 7 - 1
netbox/ipam/models.py

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

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

@@ -1,5 +1,6 @@
 from rest_framework import serializers
 from rest_framework import serializers
 
 
+from extras.api.serializers import CustomFieldsSerializer
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 
 
 
 
@@ -24,12 +25,12 @@ class TenantGroupNestedSerializer(TenantGroupSerializer):
 # Tenants
 # Tenants
 #
 #
 
 
-class TenantSerializer(serializers.ModelSerializer):
+class TenantSerializer(CustomFieldsSerializer, serializers.ModelSerializer):
     group = TenantGroupNestedSerializer()
     group = TenantGroupNestedSerializer()
 
 
     class Meta:
     class Meta:
         model = Tenant
         model = Tenant
-        fields = ['id', 'name', 'slug', 'group', 'comments']
+        fields = ['id', 'name', 'slug', 'group', 'comments', 'custom_fields']
 
 
 
 
 class TenantNestedSerializer(TenantSerializer):
 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.models import Tenant, TenantGroup
 from tenancy.filters import TenantFilter
 from tenancy.filters import TenantFilter
 
 
+from extras.api.views import CustomFieldModelAPIView
 from . import serializers
 from . import serializers
 
 
 
 
@@ -22,18 +23,18 @@ class TenantGroupDetailView(generics.RetrieveAPIView):
     serializer_class = serializers.TenantGroupSerializer
     serializer_class = serializers.TenantGroupSerializer
 
 
 
 
-class TenantListView(generics.ListAPIView):
+class TenantListView(CustomFieldModelAPIView, generics.ListAPIView):
     """
     """
     List tenants (filterable)
     List tenants (filterable)
     """
     """
-    queryset = Tenant.objects.select_related('group')
+    queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values')
     serializer_class = serializers.TenantSerializer
     serializer_class = serializers.TenantSerializer
     filter_class = TenantFilter
     filter_class = TenantFilter
 
 
 
 
-class TenantDetailView(generics.RetrieveAPIView):
+class TenantDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView):
     """
     """
     Retrieve a single tenant
     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
     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.core.urlresolvers import reverse
 from django.db import models
 from django.db import models
 
 
-from extras.models import CustomFieldModel
+from extras.models import CustomFieldModel, CustomFieldValue
 from utilities.models import CreatedUpdatedModel
 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)
     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)")
     description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)")
     comments = models.TextField(blank=True)
     comments = models.TextField(blank=True)
+    custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
 
 
     class Meta:
     class Meta:
         ordering = ['group', 'name']
         ordering = ['group', 'name']