33import re
44from collections import defaultdict
55
6+ from cache_memoize import cache_memoize
67from django .core .exceptions import ObjectDoesNotExist , PermissionDenied , ValidationError
78from django .db import models , transaction
89from django .utils .translation import gettext_lazy as _
@@ -100,8 +101,15 @@ class AbstractConfig(ChecksumCacheMixin, BaseConfig):
100101 load_kwargs = {"object_pairs_hook" : collections .OrderedDict },
101102 dump_kwargs = {"indent" : 4 },
102103 )
104+ checksum_db = models .CharField (
105+ _ ("configuration checksum" ),
106+ max_length = 32 ,
107+ blank = True ,
108+ null = True ,
109+ help_text = _ ("Checksum of the generated configuration." ),
110+ )
103111
104- _CHECKSUM_CACHE_TIMEOUT = 60 * 60 * 24 * 30 # 10 days
112+ _CHECKSUM_CACHE_TIMEOUT = ChecksumCacheMixin . _CHECKSUM_CACHE_TIMEOUT
105113 _config_context_functions = list ()
106114 _old_backend = None
107115
@@ -151,6 +159,32 @@ def key(self):
151159 """
152160 return self .device .key
153161
162+ @cache_memoize (
163+ timeout = ChecksumCacheMixin ._CHECKSUM_CACHE_TIMEOUT ,
164+ args_rewrite = get_cached_args_rewrite ,
165+ )
166+ def get_cached_checksum (self ):
167+ """
168+ Returns the cached configuration checksum.
169+
170+ Unlike `ChecksumCacheMixin.get_cached_checksum`, this returns the
171+ value from the `checksum_db` field instead of recalculating it.
172+ """
173+ self .refresh_from_db (fields = ["checksum_db" ])
174+ return self .checksum_db
175+
176+ @classmethod
177+ def bulk_invalidate_get_cached_checksum (cls , query_params ):
178+ """
179+ Bulk invalidates cached configuration checksums for matching instances
180+
181+ Sets status to modified if the configuration of the instance has changed.
182+ """
183+ for instance in cls .objects .only ("id" ).filter (** query_params ).iterator ():
184+ has_changed = instance .update_status_if_checksum_changed ()
185+ if has_changed :
186+ instance .invalidate_checksum_cache ()
187+
154188 @classmethod
155189 def get_template_model (cls ):
156190 return cls .templates .rel .model
@@ -276,9 +310,9 @@ def templates_changed(cls, action, instance, **kwargs):
276310 if not instance ._just_created :
277311 # sends only config modified signal
278312 instance ._send_config_modified_signal (action = "m2m_templates_changed" )
279- if instance .status != "modified" :
280- # sends both status modified and config modified signals
281- instance . set_status_modified ( send_config_modified_signal = False )
313+ instance .update_status_if_checksum_changed (
314+ send_config_modified_signal = False
315+ )
282316
283317 @classmethod
284318 def manage_vpn_clients (cls , action , instance , pk_set , ** kwargs ):
@@ -443,7 +477,7 @@ def certificate_updated(cls, instance, created, **kwargs):
443477 except ObjectDoesNotExist :
444478 return
445479 else :
446- transaction .on_commit (config .set_status_modified )
480+ transaction .on_commit (config .update_status_if_checksum_changed )
447481
448482 @classmethod
449483 def register_context_function (cls , func ):
@@ -539,7 +573,9 @@ def clean(self):
539573 def save (self , * args , ** kwargs ):
540574 created = self ._state .adding
541575 # check if config has been modified (so we can emit signals)
542- if not created :
576+ if created :
577+ self .checksum_db = self .checksum
578+ else :
543579 self ._check_changes ()
544580 self ._just_created = created
545581 result = super ().save (* args , ** kwargs )
@@ -584,15 +620,63 @@ def _check_changes(self):
584620 if self .backend != current .backend :
585621 # storing old backend to send backend change signal after save
586622 self ._old_backend = current .backend
587- if hasattr (self , "backend_instance" ):
588- del self .backend_instance
589- if self .checksum != current .checksum :
623+ self .update_status_if_checksum_changed (
624+ save = False ,
625+ )
626+
627+ def update_status_if_checksum_changed (
628+ self , save = True , update_checksum_db = True , send_config_modified_signal = True
629+ ):
630+ """
631+ Updates the instance status if its checksum has changed.
632+
633+ Returns:
634+ bool: True if the checksum changed and an update was applied,
635+ False otherwise.
636+ """
637+ checksum_changed = self ._has_configuration_checksum_changed ()
638+ if checksum_changed :
639+ self .checksum_db = self .checksum
590640 if self .status != "modified" :
591- self .set_status_modified (save = False )
641+ self .set_status_modified (
642+ save = save ,
643+ send_config_modified_signal = send_config_modified_signal ,
644+ extra_update_fields = ["checksum_db" ],
645+ )
592646 else :
593- # config modified signal is always sent
594- # regardless of the current status
595- self ._send_config_modified_after_save = True
647+ if update_checksum_db :
648+ self ._update_checksum_db (new_checksum = self .checksum_db )
649+ if send_config_modified_signal :
650+ self ._send_config_modified_after_save = True
651+ self .invalidate_checksum_cache ()
652+ return checksum_changed
653+
654+ def _has_configuration_checksum_changed (self ):
655+ """
656+ Determines whether the config checksum has changed
657+
658+ Returns True if:
659+ - No checksum_db exists (first time)
660+ - Current checksum differs from checksum_db
661+
662+ Returns False if:
663+ - Current checksum is the same as checksum_db
664+ """
665+ if self .checksum_db is None :
666+ # First time or no database checksum, should update
667+ return True
668+ self ._invalidate_backend_instance_cache ()
669+ return self .checksum_db != self .checksum
670+
671+ def _update_checksum_db (self , new_checksum = None ):
672+ """
673+ Updates checksum_db field in the database
674+
675+ It does not call save() to avoid sending signals
676+ and updating other fields.
677+ """
678+ new_checksum = new_checksum or self .checksum
679+ self ._meta .model .objects .filter (pk = self .pk ).update (checksum_db = new_checksum )
596680
597681 def _send_config_modified_signal (self , action ):
598682 """
@@ -668,10 +752,12 @@ def _set_status(self, status, save=True, reason=None, extra_update_fields=None):
668752 if save :
669753 self .save (update_fields = update_fields )
670754
671- def set_status_modified (self , save = True , send_config_modified_signal = True ):
755+ def set_status_modified (
756+ self , save = True , send_config_modified_signal = True , extra_update_fields = None
757+ ):
672758 if send_config_modified_signal :
673759 self ._send_config_modified_after_save = True
674- self ._set_status ("modified" , save )
760+ self ._set_status ("modified" , save , extra_update_fields = extra_update_fields )
675761
676762 def set_status_applied (self , save = True ):
677763 self ._set_status ("applied" , save )
@@ -697,12 +783,22 @@ def deactivate(self):
697783 """
698784 # Invalidate cached property before checking checksum.
699785 self ._invalidate_backend_instance_cache ()
700- old_checksum = self .checksum
786+ old_checksum = self .checksum_db
787+ # Don't alter the order of the following steps.
788+ # We need to set the status to deactivating before clearing the templates
789+ # otherwise, the "enforce_required_templates" and "add_default_templates"
790+ # methods would re-add required/default templates.
791+ # The "templates_changed" receiver skips post_clear action. Thus,
792+ # we need to update the checksum_db field manually and invalidate
793+ # the cache.
701794 self .config = {}
702- self .set_status_deactivating ()
795+ self .set_status_deactivating (save = False )
703796 self .templates .clear ()
704- del self .backend_instance
705- if old_checksum == self .checksum :
797+ self ._invalidate_backend_instance_cache ()
798+ self .checksum_db = self .checksum
799+ self .invalidate_checksum_cache ()
800+ self .save ()
801+ if old_checksum == self .checksum_db :
706802 # Accelerate deactivation if the configuration remains
707803 # unchanged (i.e. empty configuration)
708804 self .set_status_deactivated ()
0 commit comments