Przeglądaj źródła

Merge pull request #3069 from digitalocean/2647-caching

intial work on #2647 - caching
Jeremy Stretch 6 lat temu
rodzic
commit
1877afc760

+ 1 - 0
.travis.yml

@@ -1,6 +1,7 @@
 sudo: required
 services:
   - postgresql
+  - redis-server
 addons:
   postgresql: "9.4"
 language: python

+ 4 - 0
base_requirements.txt

@@ -18,6 +18,10 @@ django-filter
 # https://github.com/django-mptt/django-mptt
 django-mptt
 
+# Django caching using Redis
+# https://github.com/niwinz/django-redis
+django-redis
+
 # Abstraction models for rendering and paginating HTML tables
 # https://github.com/jieter/django-tables2
 django-tables2

+ 5 - 0
netbox/circuits/views.py

@@ -1,9 +1,12 @@
+from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.decorators import permission_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db import transaction
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 
 from extras.models import Graph, GRAPH_TYPE_PROVIDER
@@ -32,6 +35,7 @@ class ProviderListView(PermissionRequiredMixin, ObjectListView):
 class ProviderView(PermissionRequiredMixin, View):
     permission_required = 'circuits.view_provider'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, slug):
 
         provider = get_object_or_404(Provider, slug=slug)
@@ -147,6 +151,7 @@ class CircuitListView(PermissionRequiredMixin, ObjectListView):
 class CircuitView(PermissionRequiredMixin, View):
     permission_required = 'circuits.view_circuit'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)

+ 22 - 0
netbox/dcim/views.py

@@ -13,6 +13,8 @@ from django.urls import reverse
 from django.utils.html import escape
 from django.utils.http import is_safe_url
 from django.utils.safestring import mark_safe
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 
 from circuits.models import Circuit
@@ -195,6 +197,7 @@ class SiteListView(PermissionRequiredMixin, ObjectListView):
 class SiteView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_site'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, slug):
 
         site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
@@ -353,6 +356,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
     """
     permission_required = 'dcim.view_rack'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
 
         racks = Rack.objects.select_related(
@@ -392,6 +396,7 @@ class RackElevationListView(PermissionRequiredMixin, View):
 class RackView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_rack'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
@@ -570,6 +575,7 @@ class DeviceTypeListView(PermissionRequiredMixin, ObjectListView):
 class DeviceTypeView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_devicetype'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         devicetype = get_object_or_404(DeviceType, pk=pk)
@@ -910,6 +916,7 @@ class DeviceListView(PermissionRequiredMixin, ObjectListView):
 class DeviceView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_device'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         device = get_object_or_404(Device.objects.select_related(
@@ -991,6 +998,7 @@ class DeviceView(PermissionRequiredMixin, View):
 class DeviceInventoryView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_device'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         device = get_object_or_404(Device, pk=pk)
@@ -1012,6 +1020,7 @@ class DeviceInventoryView(PermissionRequiredMixin, View):
 class DeviceStatusView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         device = get_object_or_404(Device, pk=pk)
@@ -1025,6 +1034,7 @@ class DeviceStatusView(PermissionRequiredMixin, View):
 class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         device = get_object_or_404(Device, pk=pk)
@@ -1042,6 +1052,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
 class DeviceConfigView(PermissionRequiredMixin, View):
     permission_required = ('dcim.view_device', 'dcim.napalm_read')
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         device = get_object_or_404(Device, pk=pk)
@@ -1279,6 +1290,7 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class InterfaceView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_interface'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         interface = get_object_or_404(Interface, pk=pk)
@@ -1499,6 +1511,7 @@ class DeviceBayDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class DeviceBayPopulateView(PermissionRequiredMixin, View):
     permission_required = 'dcim.change_devicebay'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         device_bay = get_object_or_404(DeviceBay, pk=pk)
@@ -1533,6 +1546,7 @@ class DeviceBayPopulateView(PermissionRequiredMixin, View):
 class DeviceBayDepopulateView(PermissionRequiredMixin, View):
     permission_required = 'dcim.change_devicebay'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         device_bay = get_object_or_404(DeviceBay, pk=pk)
@@ -1672,6 +1686,7 @@ class CableListView(PermissionRequiredMixin, ObjectListView):
 class CableView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_cable'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         cable = get_object_or_404(Cable, pk=pk)
@@ -1687,6 +1702,7 @@ class CableTraceView(PermissionRequiredMixin, View):
     """
     permission_required = 'dcim.view_cable'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, model, pk):
 
         obj = get_object_or_404(model, pk=pk)
@@ -1726,6 +1742,7 @@ class CableCreateView(PermissionRequiredMixin, GetReturnURLMixin, View):
 
         return super().dispatch(request, *args, **kwargs)
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, *args, **kwargs):
 
         # Parse initial data manually to avoid setting field values as lists
@@ -2042,6 +2059,7 @@ class VirtualChassisCreateView(PermissionRequiredMixin, View):
 class VirtualChassisEditView(PermissionRequiredMixin, GetReturnURLMixin, View):
     permission_required = 'dcim.change_virtualchassis'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
@@ -2110,6 +2128,7 @@ class VirtualChassisDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, View):
     permission_required = 'dcim.change_virtualchassis'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         virtual_chassis = get_object_or_404(VirtualChassis, pk=pk)
@@ -2164,6 +2183,7 @@ class VirtualChassisAddMemberView(PermissionRequiredMixin, GetReturnURLMixin, Vi
 class VirtualChassisRemoveMemberView(PermissionRequiredMixin, GetReturnURLMixin, View):
     permission_required = 'dcim.change_virtualchassis'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         device = get_object_or_404(Device, pk=pk, virtual_chassis__isnull=False)
@@ -2227,6 +2247,7 @@ class PowerPanelListView(PermissionRequiredMixin, ObjectListView):
 class PowerPanelView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_powerpanel'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         powerpanel = get_object_or_404(PowerPanel.objects.select_related('site', 'rack_group'), pk=pk)
@@ -2296,6 +2317,7 @@ class PowerFeedListView(PermissionRequiredMixin, ObjectListView):
 class PowerFeedView(PermissionRequiredMixin, View):
     permission_required = 'dcim.view_powerfeed'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         powerfeed = get_object_or_404(PowerFeed.objects.select_related('power_panel', 'rack'), pk=pk)

+ 9 - 0
netbox/extras/views.py

@@ -7,6 +7,8 @@ from django.db.models import Count, Q
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.safestring import mark_safe
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django_tables2 import RequestConfig
 
@@ -41,6 +43,7 @@ class TagListView(ObjectListView):
 
 class TagView(View):
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, slug):
 
         tag = get_object_or_404(Tag, slug=slug)
@@ -108,6 +111,7 @@ class ConfigContextListView(PermissionRequiredMixin, ObjectListView):
 class ConfigContextView(PermissionRequiredMixin, View):
     permission_required = 'extras.view_configcontext'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         configcontext = get_object_or_404(ConfigContext, pk=pk)
@@ -155,6 +159,7 @@ class ObjectConfigContextView(View):
     object_class = None
     base_template = None
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         obj = get_object_or_404(self.object_class, pk=pk)
@@ -187,6 +192,7 @@ class ObjectChangeListView(PermissionRequiredMixin, ObjectListView):
 class ObjectChangeView(PermissionRequiredMixin, View):
     permission_required = 'extras.view_objectchange'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         objectchange = get_object_or_404(ObjectChange, pk=pk)
@@ -209,6 +215,7 @@ class ObjectChangeLogView(View):
     Present a history of changes made to a particular object.
     """
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, model, **kwargs):
 
         # Get object my model and kwargs (e.g. slug='foo')
@@ -282,6 +289,7 @@ class ReportListView(PermissionRequiredMixin, View):
     """
     permission_required = 'extras.view_reportresult'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
 
         reports = get_reports()
@@ -306,6 +314,7 @@ class ReportView(PermissionRequiredMixin, View):
     """
     permission_required = 'extras.view_reportresult'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, name):
 
         # Retrieve the Report by "<module>.<report>"

+ 13 - 0
netbox/ipam/views.py

@@ -3,6 +3,8 @@ from django.conf import settings
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count, Q
 from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from django_tables2 import RequestConfig
 
@@ -125,6 +127,7 @@ class VRFListView(PermissionRequiredMixin, ObjectListView):
 class VRFView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_vrf'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         vrf = get_object_or_404(VRF.objects.all(), pk=pk)
@@ -319,6 +322,7 @@ class AggregateListView(PermissionRequiredMixin, ObjectListView):
 class AggregateView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_aggregate'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         aggregate = get_object_or_404(Aggregate, pk=pk)
@@ -456,6 +460,7 @@ class PrefixListView(PermissionRequiredMixin, ObjectListView):
 class PrefixView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_prefix'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         prefix = get_object_or_404(Prefix.objects.select_related(
@@ -500,6 +505,7 @@ class PrefixView(PermissionRequiredMixin, View):
 class PrefixPrefixesView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_prefix'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
@@ -543,6 +549,7 @@ class PrefixPrefixesView(PermissionRequiredMixin, View):
 class PrefixIPAddressesView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_prefix'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
@@ -643,6 +650,7 @@ class IPAddressListView(PermissionRequiredMixin, ObjectListView):
 class IPAddressView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_ipaddress'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         ipaddress = get_object_or_404(IPAddress.objects.select_related('vrf__tenant', 'tenant'), pk=pk)
@@ -726,6 +734,7 @@ class IPAddressAssignView(PermissionRequiredMixin, View):
 
         return super().dispatch(request, *args, **kwargs)
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
 
         form = forms.IPAddressAssignForm()
@@ -838,6 +847,7 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 class VLANGroupVLANsView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_vlangroup'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         vlan_group = get_object_or_404(VLANGroup.objects.all(), pk=pk)
@@ -888,6 +898,7 @@ class VLANListView(PermissionRequiredMixin, ObjectListView):
 class VLANView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_vlan'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         vlan = get_object_or_404(VLAN.objects.select_related(
@@ -906,6 +917,7 @@ class VLANView(PermissionRequiredMixin, View):
 class VLANMembersView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_vlan'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         vlan = get_object_or_404(VLAN.objects.all(), pk=pk)
@@ -984,6 +996,7 @@ class ServiceListView(PermissionRequiredMixin, ObjectListView):
 class ServiceView(PermissionRequiredMixin, View):
     permission_required = 'ipam.view_service'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         service = get_object_or_404(Service, pk=pk)

+ 22 - 10
netbox/netbox/configuration.example.py

@@ -25,6 +25,16 @@ DATABASE = {
 # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECRET_KEY
 SECRET_KEY = ''
 
+# Redis database settings. The Redis database is used for caching and background processing such as webhooks
+REDIS = {
+    'HOST': 'localhost',
+    'PORT': 6379,
+    'PASSWORD': '',
+    'DATABASE': 0,
+    'DEFAULT_TIMEOUT': 300,
+    'SSL': False,
+}
+
 
 #########################
 #                       #
@@ -50,6 +60,18 @@ BANNER_LOGIN = ''
 # BASE_PATH = 'netbox/'
 BASE_PATH = ''
 
+# The fraction of entries that are culled when CACHE_MAX_ENTRIES is reached. The actual ratio is 1 / CACHE_CULL_FREQUENCY,
+# so set CACHE_CULL_FREQUENCY to 2 to cull half the entries when CACHE_MAX_ENTRIES is reached. This setting should be an
+# integer and defaults to 3
+CACHE_CULL_FREQUENCY = 3
+
+# Max number of entries (unique pages) to store in the cache at a time.
+CACHE_MAX_ENTRIES = 300
+
+# Cache timeout in seconds. Set to `None` to enforce an infinate timeout. Set to 0 to dissable caching by immediatly
+# expiring keys. Defaults to 900 (15 minutes)
+CACHE_TIMEOUT = 900
+
 # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90)
 CHANGELOG_RETENTION = 90
 
@@ -133,16 +155,6 @@ PAGINATE_COUNT = 50
 # prefer IPv4 instead.
 PREFER_IPV4 = False
 
-# Redis database settings (optional). A Redis database is required only if the webhooks backend is enabled.
-REDIS = {
-    'HOST': 'localhost',
-    'PORT': 6379,
-    'PASSWORD': '',
-    'DATABASE': 0,
-    'DEFAULT_TIMEOUT': 300,
-    'SSL': False,
-}
-
 # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of
 # this setting is derived from the installed location.
 # REPORTS_ROOT = '/opt/netbox/netbox/reports'

+ 30 - 1
netbox/netbox/settings.py

@@ -28,7 +28,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
 # Import required configuration parameters
 ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
-for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
+for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY', 'REDIS']:
     try:
         globals()[setting] = getattr(configuration, setting)
     except AttributeError:
@@ -44,6 +44,9 @@ BANNER_TOP = getattr(configuration, 'BANNER_TOP', '')
 BASE_PATH = getattr(configuration, 'BASE_PATH', '')
 if BASE_PATH:
     BASE_PATH = BASE_PATH.strip('/') + '/'  # Enforce trailing slash only
+CACHE_TIMEOUT = getattr(configuration, 'CACHE_TIMEOUT', 900)
+CACHE_MAX_ENTRIES = getattr(configuration, 'CACHE_MAX_ENTRIES', 300)
+CACHE_CULL_FREQUENCY = getattr(configuration, 'CACHE_CULL_FREQUENCY', 3)
 CHANGELOG_RETENTION = getattr(configuration, 'CHANGELOG_RETENTION', 90)
 CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
 CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
@@ -157,6 +160,7 @@ INSTALLED_APPS = [
     'django.contrib.staticfiles',
     'django.contrib.humanize',
     'corsheaders',
+    'django_redis',
     'debug_toolbar',
     'django_filters',
     'django_tables2',
@@ -218,6 +222,31 @@ TEMPLATES = [
     },
 ]
 
+# Caching
+if REDIS_SSL:
+    REDIS_CACHE_CON_STRING = 'rediss://'
+else:
+    REDIS_CACHE_CON_STRING = 'redis://'
+
+if REDIS_PASSWORD:
+    REDIS_CACHE_CON_STRING = '{}@{}'.format(REDIS_PASSWORD, REDIS_CACHE_CON_STRING)
+
+REDIS_CACHE_CON_STRING = '{}{}:{}/{}'.format(REDIS_CACHE_CON_STRING, REDIS_HOST, REDIS_PORT, REDIS_DATABASE)
+CACHE_BACKEND = 'django_redis.cache.RedisCache'
+
+CACHES = {
+    "default": {
+        "BACKEND": CACHE_BACKEND,
+        "LOCATION": REDIS_CACHE_CON_STRING,
+        'TIMEOUT': CACHE_TIMEOUT,
+        "OPTIONS": {
+            "CLIENT_CLASS": "django_redis.client.DefaultClient",
+            "MAX_ENTRIES": CACHE_MAX_ENTRIES,
+            "CULL_FREQUENCY": CACHE_CULL_FREQUENCY
+        }
+    }
+}
+
 # WSGI
 WSGI_APPLICATION = 'netbox.wsgi.application'
 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

+ 6 - 0
netbox/netbox/views.py

@@ -1,7 +1,10 @@
 from collections import OrderedDict
 
+from django.conf import settings
 from django.db.models import Count, F
 from django.shortcuts import render
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 from rest_framework.response import Response
 from rest_framework.reverse import reverse
@@ -160,6 +163,7 @@ SEARCH_TYPES = OrderedDict((
 class HomeView(View):
     template_name = 'home.html'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
 
         connected_consoleports = ConsolePort.objects.filter(
@@ -219,6 +223,7 @@ class HomeView(View):
 
 class SearchView(View):
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
 
         # No query
@@ -272,6 +277,7 @@ class APIRootView(APIView):
     def get_view_name(self):
         return "API Root"
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, format=None):
 
         return Response(OrderedDict((

+ 3 - 0
netbox/secrets/views.py

@@ -1,5 +1,6 @@
 import base64
 
+from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.decorators import permission_required, login_required
 from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -7,6 +8,7 @@ from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 
 from dcim.models import Device
@@ -80,6 +82,7 @@ class SecretListView(PermissionRequiredMixin, ObjectListView):
 class SecretView(PermissionRequiredMixin, View):
     permission_required = 'secrets.view_secret'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         secret = get_object_or_404(Secret, pk=pk)

+ 4 - 0
netbox/tenancy/views.py

@@ -1,6 +1,9 @@
+from django.conf import settings
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, render
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 
 from circuits.models import Circuit
@@ -66,6 +69,7 @@ class TenantListView(PermissionRequiredMixin, ObjectListView):
 class TenantView(PermissionRequiredMixin, View):
     permission_required = 'tenancy.view_tenant'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, slug):
 
         tenant = get_object_or_404(Tenant, slug=slug)

+ 18 - 0
netbox/utilities/api.py

@@ -6,6 +6,8 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models import ManyToManyField
 from django.http import Http404
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from rest_framework.exceptions import APIException
 from rest_framework.permissions import BasePermission
 from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
@@ -248,6 +250,20 @@ class ModelViewSet(_ModelViewSet):
         # Fall back to the hard-coded serializer class
         return self.serializer_class
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
+    def list(self, *args, **kwargs):
+        """
+        Call to super to allow for caching
+        """
+        return super().list(*args, **kwargs)
+
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
+    def retrieve(self, *args, **kwargs):
+        """
+        Call to super to allow for caching
+        """
+        return super().retrieve(*args, **kwargs)
+
 
 class FieldChoicesViewSet(ViewSet):
     """
@@ -284,9 +300,11 @@ class FieldChoicesViewSet(ViewSet):
                         })
                 self._fields[key] = choices
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def list(self, request):
         return Response(self._fields)
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def retrieve(self, request, pk):
         if pk not in self._fields:
             raise Http404

+ 4 - 0
netbox/utilities/views.py

@@ -17,6 +17,8 @@ from django.urls import reverse
 from django.utils.html import escape
 from django.utils.http import is_safe_url
 from django.utils.safestring import mark_safe
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.decorators.csrf import requires_csrf_token
 from django.views.defaults import ERROR_500_TEMPLATE_NAME
 from django.views.generic import View
@@ -106,6 +108,7 @@ class ObjectListView(View):
 
         return csv_data
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request):
 
         model = self.queryset.model
@@ -713,6 +716,7 @@ class ComponentCreateView(View):
     model_form = None
     template_name = None
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         parent = get_object_or_404(self.parent_model, pk=pk)

+ 6 - 0
netbox/virtualization/views.py

@@ -1,9 +1,12 @@
+from django.conf import settings
 from django.contrib import messages
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db import transaction
 from django.db.models import Count
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
+from django.utils.decorators import method_decorator
+from django.views.decorators.cache import cache_page
 from django.views.generic import View
 
 from dcim.models import Device, Interface
@@ -106,6 +109,7 @@ class ClusterListView(PermissionRequiredMixin, ObjectListView):
 class ClusterView(PermissionRequiredMixin, View):
     permission_required = 'virtualization.view_cluster'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         cluster = get_object_or_404(Cluster, pk=pk)
@@ -168,6 +172,7 @@ class ClusterAddDevicesView(PermissionRequiredMixin, View):
     form = forms.ClusterAddDevicesForm
     template_name = 'virtualization/cluster_add_devices.html'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         cluster = get_object_or_404(Cluster, pk=pk)
@@ -263,6 +268,7 @@ class VirtualMachineListView(PermissionRequiredMixin, ObjectListView):
 class VirtualMachineView(PermissionRequiredMixin, View):
     permission_required = 'virtualization.view_virtualmachine'
 
+    @method_decorator(cache_page(settings.CACHE_TIMEOUT))
     def get(self, request, pk):
 
         virtualmachine = get_object_or_404(VirtualMachine.objects.select_related('tenant__group'), pk=pk)

+ 1 - 0
requirements.txt

@@ -3,6 +3,7 @@ django-cors-headers==2.5.2
 django-debug-toolbar==1.11
 django-filter==2.1.0
 django-mptt==0.9.1
+django-redis==4.5.0
 django-tables2==2.0.6
 django-taggit==1.1.0
 django-taggit-serializer==0.1.7