diff --git a/.travis.yml b/.travis.yml index 13c6d406b..29fa87b64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ sudo: required services: - postgresql + - redis-server addons: postgresql: "9.4" language: python diff --git a/base_requirements.txt b/base_requirements.txt index 3d1578400..2aa97f708 100644 --- a/base_requirements.txt +++ b/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 diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 24b4259f4..3aa99c133 100644 --- a/netbox/circuits/views.py +++ b/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) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c82f795b5..952c5a778 100644 --- a/netbox/dcim/views.py +++ b/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) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 21ea4f1c9..cb9b40cdd 100644 --- a/netbox/extras/views.py +++ b/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 "." diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index d80646bb0..c2ab13301 100644 --- a/netbox/ipam/views.py +++ b/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) diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 6efecdf35..df8e1afd0 100644 --- a/netbox/netbox/configuration.example.py +++ b/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' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 69a9c13f7..58bd209e0 100644 --- a/netbox/netbox/settings.py +++ b/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') diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index a0e8d6a7e..cdd34f12b 100644 --- a/netbox/netbox/views.py +++ b/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(( diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index 74772beb0..aeab49c8b 100644 --- a/netbox/secrets/views.py +++ b/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) diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 5d43309de..627f32855 100644 --- a/netbox/tenancy/views.py +++ b/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) diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index fbebd09ff..40f14eb1f 100644 --- a/netbox/utilities/api.py +++ b/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 diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 02441549f..59a6fcafd 100644 --- a/netbox/utilities/views.py +++ b/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) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 712c7acb5..fdb4bbc10 100644 --- a/netbox/virtualization/views.py +++ b/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) diff --git a/requirements.txt b/requirements.txt index 8ef0d0a56..50d0f2ee0 100644 --- a/requirements.txt +++ b/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