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 .get_cached_checksum .invalidate (instance )
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 ):
@@ -541,6 +575,8 @@ def save(self, *args, **kwargs):
541575 # check if config has been modified (so we can emit signals)
542576 if not created :
543577 self ._check_changes ()
578+ self ._invalidate_backend_instance_cache ()
579+ self .checksum_db = self .checksum
544580 self ._just_created = created
545581 result = super ().save (* args , ** kwargs )
546582 # add default templates if config has just been created
@@ -584,8 +620,7 @@ 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
623+ self ._invalidate_backend_instance_cache ()
589624 if self .checksum != current .checksum :
590625 if self .status != "modified" :
591626 self .set_status_modified (save = False )
@@ -594,6 +629,52 @@ def _check_changes(self):
594629 # regardless of the current status
595630 self ._send_config_modified_after_save = True
596631
632+ def update_status_if_checksum_changed (
633+ self , save = True , send_config_modified_signal = True
634+ ):
635+ """
636+ Updates the instance status if its checksum has changed.
637+
638+ Returns:
639+ bool: True if the checksum changed and an update was applied,
640+ False otherwise.
641+ """
642+ checksum_changed = self ._should_update_status_based_on_checksum ()
643+ if checksum_changed :
644+ self .checksum_db = self .checksum
645+ if self .status != "modified" :
646+ self .set_status_modified (
647+ save = save ,
648+ send_config_modified_signal = send_config_modified_signal ,
649+ extra_update_fields = ["checksum_db" ],
650+ )
651+ else :
652+ # Instead of calling the "save()" method, which would
653+ # trigger various signals and checks, we directly update
654+ # the "checksum_db" field in the database.
655+ self ._meta .model .objects .filter (pk = self .pk ).update (
656+ checksum_db = self .checksum_db
657+ )
658+ return checksum_changed
659+
660+ def _should_update_status_based_on_checksum (self ):
661+ """
662+ Determines whether the config status should be updated based on
663+ checksum comparison.
664+
665+ Returns True if:
666+ - No checksum_db exists (first time)
667+ - Current checksum differs from checksum_db
668+
669+ Returns False if:
670+ - Current checksum is the same as checksum_db
671+ """
672+ if self .checksum_db is None :
673+ # First time or no database checksum, should update
674+ return True
675+ self ._invalidate_backend_instance_cache ()
676+ return self .checksum_db != self .checksum
677+
597678 def _send_config_modified_signal (self , action ):
598679 """
599680 Emits ``config_modified`` signal.
@@ -668,10 +749,12 @@ def _set_status(self, status, save=True, reason=None, extra_update_fields=None):
668749 if save :
669750 self .save (update_fields = update_fields )
670751
671- def set_status_modified (self , save = True , send_config_modified_signal = True ):
752+ def set_status_modified (
753+ self , save = True , send_config_modified_signal = True , extra_update_fields = None
754+ ):
672755 if send_config_modified_signal :
673756 self ._send_config_modified_after_save = True
674- self ._set_status ("modified" , save )
757+ self ._set_status ("modified" , save , extra_update_fields )
675758
676759 def set_status_applied (self , save = True ):
677760 self ._set_status ("applied" , save )
0 commit comments