Skip to content

Commit e6f28c6

Browse files
committed
[fix] Fixed perennial config modified state #1113
Fixes #1113
1 parent 5d25d8a commit e6f28c6

File tree

5 files changed

+177
-22
lines changed

5 files changed

+177
-22
lines changed

openwisp_controller/config/base/config.py

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ class AbstractConfig(ChecksumCacheMixin, BaseConfig):
100100
load_kwargs={"object_pairs_hook": collections.OrderedDict},
101101
dump_kwargs={"indent": 4},
102102
)
103+
checksum_db = models.CharField(
104+
_("database checksum"),
105+
max_length=32,
106+
blank=True,
107+
null=True,
108+
help_text=_(
109+
"Last known checksum stored in database for optimization purposes."
110+
),
111+
)
103112

104113
_CHECKSUM_CACHE_TIMEOUT = 60 * 60 * 24 * 30 # 10 days
105114
_config_context_functions = list()
@@ -265,7 +274,15 @@ def templates_changed(cls, action, instance, **kwargs):
265274
there fore we need to ignore it to avoid emitting signals twice
266275
"""
267276
# execute only after a config has been saved or deleted
268-
if action not in ["post_add", "post_remove"] or instance._state.adding:
277+
if (
278+
action not in ["post_add", "post_remove", "post_clear"]
279+
or instance._state.adding
280+
):
281+
return
282+
if action == "post_clear":
283+
if hasattr(instance, "backend_instance"):
284+
del instance.backend_instance
285+
cls.objects.filter(pk=instance.pk).update(checksum_db=instance.checksum)
269286
return
270287
# use atomic to ensure any code bound to
271288
# be executed via transaction.on_commit
@@ -276,9 +293,9 @@ def templates_changed(cls, action, instance, **kwargs):
276293
if not instance._just_created:
277294
# sends only config modified signal
278295
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)
296+
instance.check_and_update_status_if_changed(
297+
send_config_modified_signal=False
298+
)
282299

283300
@classmethod
284301
def manage_vpn_clients(cls, action, instance, pk_set, **kwargs):
@@ -538,9 +555,11 @@ def clean(self):
538555

539556
def save(self, *args, **kwargs):
540557
created = self._state.adding
541-
# check if config has been modified (so we can emit signals)
542558
if not created:
543559
self._check_changes()
560+
if hasattr(self, "backend_instance"):
561+
del self.backend_instance
562+
self.checksum_db = self.checksum
544563
self._just_created = created
545564
result = super().save(*args, **kwargs)
546565
# add default templates if config has just been created
@@ -579,7 +598,10 @@ def is_deactivated(self):
579598

580599
def _check_changes(self):
581600
current = self._meta.model.objects.only(
582-
"backend", "config", "context", "status"
601+
"backend",
602+
"config",
603+
"context",
604+
"status",
583605
).get(pk=self.pk)
584606
if self.backend != current.backend:
585607
# storing old backend to send backend change signal after save
@@ -594,6 +616,48 @@ def _check_changes(self):
594616
# regardless of the current status
595617
self._send_config_modified_after_save = True
596618

619+
def check_and_update_status_if_changed(
620+
self, save=True, send_config_modified_signal=True
621+
):
622+
"""
623+
Helper method to check if configuration has changed based on checksum
624+
comparison and update status accordingly.
625+
"""
626+
627+
checksum_changed = self._should_update_status_based_on_checksum()
628+
if checksum_changed:
629+
self.checksum_db = self.checksum
630+
if self.status != "modified":
631+
self.set_status_modified(
632+
save=save,
633+
send_config_modified_signal=send_config_modified_signal,
634+
extra_update_fields=["checksum_db"],
635+
)
636+
else:
637+
self._meta.model.objects.filter(pk=self.pk).update(
638+
checksum_db=self.checksum_db
639+
)
640+
return checksum_changed
641+
642+
def _should_update_status_based_on_checksum(self):
643+
"""
644+
Determines whether the config status should be updated based on
645+
checksum comparison.
646+
647+
Returns True if:
648+
- No checksum_db exists (first time)
649+
- Current checksum differs from checksum_db
650+
651+
Returns False if:
652+
- Current checksum is the same as checksum_db
653+
"""
654+
if self.checksum_db is None:
655+
# First time or no database checksum, should update
656+
return True
657+
if hasattr(self, "backend_instance"):
658+
delattr(self, "backend_instance")
659+
return self.checksum_db != self.checksum
660+
597661
def _send_config_modified_signal(self, action):
598662
"""
599663
Emits ``config_modified`` signal.
@@ -668,10 +732,12 @@ def _set_status(self, status, save=True, reason=None, extra_update_fields=None):
668732
if save:
669733
self.save(update_fields=update_fields)
670734

671-
def set_status_modified(self, save=True, send_config_modified_signal=True):
735+
def set_status_modified(
736+
self, save=True, send_config_modified_signal=True, extra_update_fields=None
737+
):
672738
if send_config_modified_signal:
673739
self._send_config_modified_after_save = True
674-
self._set_status("modified", save)
740+
self._set_status("modified", save, extra_update_fields)
675741

676742
def set_status_applied(self, save=True):
677743
self._set_status("applied", save)

openwisp_controller/config/base/template.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -162,20 +162,18 @@ def _update_related_config_status(self):
162162
# be executed via transaction.on_commit
163163
# is executed after the whole block
164164
with transaction.atomic():
165-
changing_status = list(
166-
self.config_relations.exclude(status="modified").values_list(
167-
"pk", flat=True
165+
for config in (
166+
self.config_relations.prefetch_related(
167+
"vpnclient_set",
168+
"templates",
169+
)
170+
.select_related("device", "device__organization__config_settings")
171+
.iterator(chunk_size=1000)
172+
):
173+
config.check_and_update_status_if_changed(
174+
send_config_modified_signal=False
168175
)
169-
)
170-
# flag all related configs as modified with 1 update query
171-
self.config_relations.exclude(status="modified").update(status="modified")
172-
# the following loop should not take too long
173-
for config in self.config_relations.select_related("device").iterator():
174-
# config modified signal sent regardless
175176
config._send_config_modified_signal(action="related_template_changed")
176-
# config status changed signal sent only if status changed
177-
if config.pk in changing_status:
178-
config._send_config_status_changed_signal()
179177

180178
def _auto_add_to_existing_configs(self):
181179
"""
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Generated by Django for issue #1113 optimization
2+
3+
from django.db import migrations, models
4+
5+
6+
def populate_checksum_db(apps, schema_editor):
7+
"""
8+
Populate checksum_db field with current checksum values
9+
for existing Config objects.
10+
"""
11+
Config = apps.get_model("config", "Config")
12+
batch = []
13+
batch_size = 500
14+
qs = Config.objects.filter(checksum_db__isnull=True).iterator()
15+
for config in qs:
16+
config.checksum_db = config.checksum
17+
batch.append(config)
18+
if len(batch) >= batch_size:
19+
Config.objects.bulk_update(batch, ["checksum_db"])
20+
batch = []
21+
if batch:
22+
Config.objects.bulk_update(batch, ["checksum_db"])
23+
24+
25+
class Migration(migrations.Migration):
26+
dependencies = [
27+
("config", "0060_cleanup_api_task_notification_types"),
28+
]
29+
30+
operations = [
31+
migrations.AddField(
32+
model_name="config",
33+
name="checksum_db",
34+
field=models.CharField(
35+
blank=True,
36+
help_text=(
37+
"Last known checksum stored in database for "
38+
"optimization purposes."
39+
),
40+
max_length=32,
41+
null=True,
42+
verbose_name="database checksum",
43+
),
44+
),
45+
migrations.RunPython(
46+
populate_checksum_db,
47+
reverse_code=migrations.RunPython.noop,
48+
),
49+
]

openwisp_controller/config/tests/test_template.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ def test_config_status_modified_after_change(self):
586586
with catch_signal(config_status_changed) as handler:
587587
t.config["interfaces"][0]["name"] = "eth2"
588588
t.full_clean()
589-
with self.assertNumQueries(10):
589+
with self.assertNumQueries(13):
590590
t.save()
591591
c.refresh_from_db()
592592
handler.assert_not_called()
@@ -596,7 +596,9 @@ def test_config_modified_signal(self):
596596
conf = self._create_config(device=self._create_device(name="test-status"))
597597
template1 = self._create_template(name="t1")
598598
template2 = self._create_template(
599-
name="t2", organization=conf.device.organization
599+
name="t2",
600+
organization=conf.device.organization,
601+
config={"interfaces": [{"name": "eth1", "type": "ethernet"}]},
600602
)
601603
self.assertEqual(conf.status, "modified")
602604
# refresh instance to reset _just_created attribute
@@ -710,6 +712,33 @@ def test_config_modified_signal(self):
710712
)
711713
self.assertEqual(conf.status, "modified")
712714

715+
def test_config_status_after_template_variables_change(self):
716+
template = self._create_template(
717+
default_values={"type": "ethernet"},
718+
config={"interfaces": [{"name": "eth0", "type": "{{ type }}"}]},
719+
default=True,
720+
)
721+
config = self._create_config(
722+
device=self._create_device(name="test-status"), context={"type": "virtual"}
723+
)
724+
config.set_status_applied()
725+
config.refresh_from_db()
726+
self.assertEqual(config.status, "applied")
727+
728+
if hasattr(config, "backend_instance"):
729+
del config.backend_instance
730+
731+
template.default_values["type"] = "virtual"
732+
template.full_clean()
733+
template.save()
734+
735+
# The configuration status should not change because the device
736+
# is overriding Template.default_value
737+
config.refresh_from_db()
738+
if hasattr(config, "backend_instance"):
739+
del config.backend_instance
740+
self.assertEqual(config.status, "applied")
741+
713742
@mock.patch.object(update_template_related_config_status, "delay")
714743
def test_task_called(self, mocked_task):
715744
with self.subTest("task not called when template is created"):

tests/openwisp2/sample_config/migrations/0001_initial.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,19 @@ class Migration(migrations.Migration):
129129
),
130130
),
131131
("details", models.CharField(blank=True, max_length=64, null=True)),
132+
(
133+
"checksum_db",
134+
models.CharField(
135+
blank=True,
136+
help_text=(
137+
"Last known checksum stored in database for "
138+
"optimization purposes."
139+
),
140+
max_length=32,
141+
null=True,
142+
verbose_name="database checksum",
143+
),
144+
),
132145
],
133146
options={
134147
"verbose_name": "configuration",

0 commit comments

Comments
 (0)