Skip to content

Commit 6b33d73

Browse files
authored
[feature] Cache VPN server checksum #1064
Closes #1064
1 parent d91e11d commit 6b33d73

File tree

16 files changed

+362
-72
lines changed

16 files changed

+362
-72
lines changed

openwisp_controller/config/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,8 @@ def download_view(self, request, pk):
312312
config = instance.config
313313
else:
314314
raise Http404()
315-
config_archive = config.generate()
315+
get_configuration = getattr(config, "get_cached_configuration", config.generate)
316+
config_archive = get_configuration()
316317
return send_file(
317318
filename="{0}.tar.gz".format(config.name),
318319
contents=config_archive.getvalue(),

openwisp_controller/config/apps.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ def enable_cache_invalidation(self):
264264
Triggers the cache invalidation for the
265265
device config checksum (view and model method)
266266
"""
267-
from .controller.views import DeviceChecksumView
267+
from .controller.views import DeviceChecksumView, GetVpnView
268268
from .handlers import (
269269
device_cache_invalidation_handler,
270270
devicegroup_change_handler,
@@ -293,6 +293,21 @@ def enable_cache_invalidation(self):
293293
DeviceChecksumView.invalidate_checksum_cache,
294294
dispatch_uid="invalidate_checksum_cache",
295295
)
296+
# VPN cache invalidation
297+
post_save.connect(
298+
GetVpnView.invalidate_get_vpn_cache,
299+
sender=self.vpn_model,
300+
dispatch_uid="invalidate_get_vpn_cache",
301+
)
302+
pre_delete.connect(
303+
GetVpnView.invalidate_get_vpn_cache,
304+
sender=self.vpn_model,
305+
dispatch_uid="vpn_server_pre_delete_invalidate_get_vpn_cache",
306+
)
307+
vpn_server_modified.connect(
308+
GetVpnView.invalidate_get_vpn_cache,
309+
dispatch_uid="vpn_server_modified_invalidate_get_vpn_cache",
310+
)
296311
device_group_changed.connect(
297312
devicegroup_change_handler,
298313
sender=self.device_model,

openwisp_controller/config/base/base.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import collections
22
import hashlib
33
import json
4+
import logging
45
from copy import deepcopy
56

7+
from cache_memoize import cache_memoize
68
from django.core.exceptions import ValidationError
79
from django.db import models
810
from django.utils.functional import cached_property
@@ -15,6 +17,82 @@
1517

1618
from .. import settings as app_settings
1719

20+
logger = logging.getLogger(__name__)
21+
22+
23+
def get_cached_args_rewrite(instance):
24+
"""
25+
Use only the PK parameter for calculating the cache key
26+
"""
27+
return instance.pk.hex
28+
29+
30+
class ChecksumCacheMixin:
31+
"""
32+
Mixin that provides caching for checksum.
33+
"""
34+
35+
_CHECKSUM_CACHE_TIMEOUT = 60 * 60 * 24 * 30 # 30 days
36+
37+
@cache_memoize(
38+
timeout=_CHECKSUM_CACHE_TIMEOUT, args_rewrite=get_cached_args_rewrite
39+
)
40+
def get_cached_checksum(self):
41+
"""
42+
Handles caching,
43+
timeout=None means value is cached indefinitely
44+
(invalidation handled on post_save/post_delete signal)
45+
"""
46+
logger.debug(f"calculating checksum for {self.__class__.__name__} ID {self.pk}")
47+
return self.checksum
48+
49+
@classmethod
50+
def bulk_invalidate_get_cached_checksum(cls, query_params):
51+
"""
52+
Bulk invalidate checksum cache for multiple instances
53+
"""
54+
for instance in cls.objects.only("id").filter(**query_params).iterator():
55+
instance.get_cached_checksum.invalidate(instance)
56+
57+
def invalidate_checksum_cache(self):
58+
"""
59+
Invalidate the checksum cache for this instance
60+
"""
61+
self.get_cached_checksum.invalidate(self)
62+
logger.debug(
63+
f"invalidated checksum cache for {self.__class__.__name__} ID {self.pk}"
64+
)
65+
66+
67+
class ConfigChecksumCacheMixin(ChecksumCacheMixin):
68+
"""
69+
Mixin that provides caching for both checksum and configuration.
70+
"""
71+
72+
@cache_memoize(
73+
timeout=ChecksumCacheMixin._CHECKSUM_CACHE_TIMEOUT,
74+
args_rewrite=get_cached_args_rewrite,
75+
)
76+
def get_cached_configuration(self):
77+
"""
78+
Returns cached configuration
79+
"""
80+
return self.generate()
81+
82+
def invalidate_configuration_cache(self):
83+
"""
84+
Invalidate the configuration cache for this instance
85+
"""
86+
self.get_cached_configuration.invalidate(self)
87+
logger.debug(
88+
f"invalidated configuration cache for {self.__class__.__name__}"
89+
f" ID {self.pk}"
90+
)
91+
92+
def invalidate_checksum_cache(self):
93+
super().invalidate_checksum_cache()
94+
self.invalidate_configuration_cache()
95+
1896

1997
class BaseModel(TimeStampedEditableModel):
2098
"""

openwisp_controller/config/base/config.py

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import re
44
from collections import defaultdict
55

6-
from cache_memoize import cache_memoize
76
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
87
from django.db import models, transaction
98
from django.utils.translation import gettext_lazy as _
@@ -24,11 +23,20 @@
2423
)
2524
from ..sortedm2m.fields import SortedManyToManyField
2625
from ..utils import get_default_templates_queryset
27-
from .base import BaseConfig
26+
from .base import BaseConfig, ChecksumCacheMixin, get_cached_args_rewrite
2827

2928
logger = logging.getLogger(__name__)
3029

3130

31+
def get_cached_checksum_args_rewrite(config):
32+
"""
33+
DEPRECATED: Use get_cached_args_rewrite instead.
34+
35+
TODO: Remove this in 1.2.0 release.
36+
"""
37+
return get_cached_args_rewrite(config)
38+
39+
3240
class TemplatesThrough(object):
3341
"""
3442
Improves string representation of m2m relationship objects
@@ -38,14 +46,7 @@ def __str__(self):
3846
return _("Relationship with {0}").format(self.template.name)
3947

4048

41-
def get_cached_checksum_args_rewrite(config):
42-
"""
43-
Use only the PK parameter for calculating the cache key
44-
"""
45-
return config.pk.hex
46-
47-
48-
class AbstractConfig(BaseConfig):
49+
class AbstractConfig(ChecksumCacheMixin, BaseConfig):
4950
"""
5051
Abstract model implementing the
5152
NetJSON DeviceConfiguration object
@@ -150,23 +151,6 @@ def key(self):
150151
"""
151152
return self.device.key
152153

153-
@cache_memoize(
154-
timeout=_CHECKSUM_CACHE_TIMEOUT, args_rewrite=get_cached_checksum_args_rewrite
155-
)
156-
def get_cached_checksum(self):
157-
"""
158-
Handles caching,
159-
timeout=None means value is cached indefinitely
160-
(invalidation handled on post_save/post_delete signal)
161-
"""
162-
logger.debug(f"calculating checksum for config ID {self.pk}")
163-
return self.checksum
164-
165-
@classmethod
166-
def bulk_invalidate_get_cached_checksum(cls, query_params):
167-
for config in cls.objects.only("id").filter(**query_params).iterator():
168-
config.get_cached_checksum.invalidate(config)
169-
170154
@classmethod
171155
def get_template_model(cls):
172156
return cls.templates.rel.model

openwisp_controller/config/base/vpn.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
trigger_zerotier_server_update,
3434
trigger_zerotier_server_update_member,
3535
)
36-
from .base import BaseConfig
36+
from .base import BaseConfig, ConfigChecksumCacheMixin
3737

3838
logger = logging.getLogger(__name__)
3939

@@ -43,7 +43,7 @@ def _peer_cache_key(vpn):
4343
return str(vpn.pk)
4444

4545

46-
class AbstractVpn(ShareableOrgMixinUniqueName, BaseConfig):
46+
class AbstractVpn(ConfigChecksumCacheMixin, ShareableOrgMixinUniqueName, BaseConfig):
4747
"""
4848
Abstract VPN model
4949
"""
@@ -281,6 +281,7 @@ def save(self, *args, **kwargs):
281281
if create_dh:
282282
transaction.on_commit(lambda: create_vpn_dh.delay(self.id))
283283
if not created and self._send_vpn_modified_after_save:
284+
self.invalidate_checksum_cache()
284285
self._send_vpn_modified_signal()
285286
self._send_vpn_modified_after_save = False
286287
# For ZeroTier VPN server, if the
@@ -308,10 +309,9 @@ def _check_changes(self):
308309
]
309310
current = self._meta.model.objects.only(*attrs).get(pk=self.pk)
310311
for attr in attrs:
311-
if getattr(self, attr) == getattr(current, attr):
312-
continue
313-
self._send_vpn_modified_after_save = True
314-
break
312+
if getattr(self, attr) != getattr(current, attr):
313+
self._send_vpn_modified_after_save = True
314+
break
315315

316316
def _send_vpn_modified_signal(self):
317317
vpn_server_modified.send(sender=self.__class__, instance=self)
@@ -687,8 +687,10 @@ def _invalidate_peer_cache(self, update=False):
687687
for backend in ["wireguard", "vxlan"]:
688688
if self._is_backend_type(backend):
689689
getattr(self, f"_get_{backend}_peers").invalidate(self)
690+
self.invalidate_checksum_cache()
690691
if update:
691692
getattr(self, f"_get_{backend}_peers")()
693+
self.get_cached_configuration()
692694
# Send signal for peers changed
693695
vpn_peers_changed.send(sender=self.__class__, instance=self)
694696

openwisp_controller/config/controller/views.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ def get_device_args_rewrite(view):
127127
return pk.hex
128128

129129

130+
def get_vpn_args_rewrite(view):
131+
"""
132+
Use only the PK parameter for calculating the cache key for VPN
133+
"""
134+
pk = view.kwargs["pk"]
135+
try:
136+
pk = uuid.UUID(pk)
137+
except ValueError:
138+
return pk
139+
return pk.hex
140+
141+
130142
class DeviceChecksumView(UpdateLastIpMixin, GetDeviceView):
131143
"""
132144
returns device's configuration checksum
@@ -463,24 +475,43 @@ class GetVpnView(SingleObjectMixin, View):
463475
model = Vpn
464476

465477
def get_object(self, *args, **kwargs):
466-
queryset = self.model.objects.select_related("organization").filter(
467-
Q(organization__is_active=True) | Q(organization__isnull=True)
468-
)
478+
queryset = self.model.objects.select_related(
479+
"organization", "ca", "cert", "subnet", "ip"
480+
).filter(Q(organization__is_active=True) | Q(organization__isnull=True))
469481
return get_object_or_404(queryset, *args, **kwargs)
470482

483+
@cache_memoize(
484+
timeout=Vpn._CHECKSUM_CACHE_TIMEOUT, args_rewrite=get_vpn_args_rewrite
485+
)
486+
def get_vpn(self):
487+
pk = self.kwargs["pk"]
488+
logger.debug(f"retrieving VPN ID {pk} from DB")
489+
return self.get_object(pk=pk)
490+
491+
@classmethod
492+
def invalidate_get_vpn_cache(cls, instance, **kwargs):
493+
"""
494+
Called from signal receiver which performs cache invalidation
495+
"""
496+
view = cls()
497+
pk = str(instance.pk.hex)
498+
view.kwargs = {"pk": pk}
499+
view.get_vpn.invalidate(view)
500+
logger.debug(f"invalidated view cache for VPN ID {pk}")
501+
471502

472503
class VpnChecksumView(GetVpnView):
473504
"""
474505
returns vpn's configuration checksum
475506
"""
476507

477508
def get(self, request, *args, **kwargs):
478-
vpn = self.get_object(*args, **kwargs)
509+
vpn = self.get_vpn()
479510
bad_request = forbid_unallowed(request, "GET", "key", vpn.key)
480511
if bad_request:
481512
return bad_request
482513
checksum_requested.send(sender=vpn.__class__, instance=vpn, request=request)
483-
return ControllerResponse(vpn.checksum, content_type="text/plain")
514+
return ControllerResponse(vpn.get_cached_checksum(), content_type="text/plain")
484515

485516

486517
class VpnDownloadConfigView(GetVpnView):
@@ -489,7 +520,7 @@ class VpnDownloadConfigView(GetVpnView):
489520
"""
490521

491522
def get(self, request, *args, **kwargs):
492-
vpn = self.get_object(*args, **kwargs)
523+
vpn = self.get_vpn()
493524
bad_request = forbid_unallowed(request, "GET", "key", vpn.key)
494525
if bad_request:
495526
return bad_request

openwisp_controller/config/handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def devicegroup_templates_change_handler(instance, **kwargs):
140140

141141
def organization_disabled_handler(instance, **kwargs):
142142
"""
143-
Asynchronously invalidates DeviceCheckView.get_device cache
143+
Asynchronously invalidates device and VPN controller views cache
144144
"""
145145
if instance.is_active:
146146
return
@@ -151,4 +151,4 @@ def organization_disabled_handler(instance, **kwargs):
151151
if instance.is_active == db_instance.is_active:
152152
# No change in is_active
153153
return
154-
tasks.invalidate_device_checksum_view_cache.delay(str(instance.id))
154+
tasks.invalidate_controller_views_cache.delay(str(instance.id))

openwisp_controller/config/tasks.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ def invalidate_devicegroup_cache_delete(instance_id, model_name, **kwargs):
9999

100100
@shared_task(base=OpenwispCeleryTask)
101101
def trigger_vpn_server_endpoint(endpoint, auth_token, vpn_id):
102+
Vpn = load_model("config", "Vpn")
103+
try:
104+
vpn = Vpn.objects.get(pk=vpn_id)
105+
except Vpn.DoesNotExist:
106+
logger.error(f"VPN Server UUID: {vpn_id} does not exist.")
107+
return
108+
109+
# Cache the configuration here makes downloading the configuration faster.
110+
vpn.get_cached_configuration()
102111
response = requests.post(
103112
endpoint,
104113
params={"key": auth_token},
@@ -149,11 +158,31 @@ def bulk_invalidate_config_get_cached_checksum(query_params):
149158

150159

151160
@shared_task(base=OpenwispCeleryTask)
152-
def invalidate_device_checksum_view_cache(organization_id):
153-
from .controller.views import DeviceChecksumView
161+
def invalidate_controller_views_cache(organization_id):
162+
"""
163+
Invalidates the cache of DeviceChecksumView, GetVpnView
164+
"""
165+
from .controller.views import DeviceChecksumView, GetVpnView
154166

155167
Device = load_model("config", "Device")
168+
Vpn = load_model("config", "Vpn")
169+
156170
for device in (
157171
Device.objects.filter(organization_id=organization_id).only("id").iterator()
158172
):
159173
DeviceChecksumView.invalidate_get_device_cache(device)
174+
175+
for vpn in (
176+
Vpn.objects.filter(organization_id=organization_id).only("id").iterator()
177+
):
178+
GetVpnView.invalidate_get_vpn_cache(vpn)
179+
180+
181+
@shared_task(base=OpenwispCeleryTask)
182+
def invalidate_device_checksum_view_cache(organization_id):
183+
"""
184+
DEPRECATED: Use invalidate_controller_views_cache instead.
185+
186+
TODO: Remove this in 1.2.0 release.
187+
"""
188+
return invalidate_controller_views_cache(organization_id)

0 commit comments

Comments
 (0)