Skip to content

Commit d60ec13

Browse files
prakash-kalwaniyamn-ram
authored andcommitted
[fix] Ensure VpnClient.post_delete fires on template detach openwisp#1221
Replace bulk QuerySet.delete() with per-instance delete loops so that post_delete signals fire correctly, triggering peer cache invalidation, certificate revocation, and IP address release.
1 parent f3c99c4 commit d60ec13

File tree

3 files changed

+78
-5
lines changed

3 files changed

+78
-5
lines changed

openwisp_controller/config/api/serializers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,14 @@ def _update_config(self, device, config_data):
202202
with transaction.atomic():
203203
vpn_list = config.templates.filter(type="vpn").values_list("vpn")
204204
if vpn_list:
205-
config.vpnclient_set.exclude(vpn__in=vpn_list).delete()
205+
# Per-instance delete ensures post_delete signals fire
206+
# (cache invalidation, cert revocation, IP release).
207+
for vpnclient in (
208+
config.vpnclient_set.exclude(vpn__in=vpn_list)
209+
.select_related("vpn", "cert", "ip")
210+
.iterator()
211+
):
212+
vpnclient.delete()
206213
config.templates.set(config_templates, clear=True)
207214
config.save()
208215
except ValidationError as error:

openwisp_controller/config/base/config.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,15 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs):
338338
if instance.is_deactivating_or_deactivated():
339339
# If the device is deactivated or in the process of deactivating, then
340340
# delete all vpn clients and return.
341-
instance.vpnclient_set.all().delete()
341+
# Per-instance delete ensures post_delete signals fire
342+
# (cache invalidation, cert revocation, IP release).
343+
with transaction.atomic():
344+
for vpnclient in (
345+
instance.vpnclient_set.select_related(
346+
"vpn", "cert", "ip"
347+
).iterator()
348+
):
349+
vpnclient.delete()
342350
return
343351

344352
vpn_client_model = cls.vpn.through
@@ -370,9 +378,19 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs):
370378
# signal is triggered again—after all templates, including the required
371379
# ones, have been fully added. At that point, we can identify and
372380
# delete VpnClient objects not linked to the final template set.
373-
instance.vpnclient_set.exclude(
374-
template_id__in=instance.templates.values_list("id", flat=True)
375-
).delete()
381+
# Per-instance delete ensures post_delete signals fire
382+
# (cache invalidation, cert revocation, IP release).
383+
with transaction.atomic():
384+
for vpnclient in (
385+
instance.vpnclient_set.exclude(
386+
template_id__in=instance.templates.values_list(
387+
"id", flat=True
388+
)
389+
)
390+
.select_related("vpn", "cert", "ip")
391+
.iterator()
392+
):
393+
vpnclient.delete()
376394

377395
if action == "post_add":
378396
for template in templates.filter(type="vpn"):

openwisp_controller/config/tests/test_vpn.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,54 @@ def test_vpn_peers_changed(self):
880880
device.config.templates.remove(template)
881881
handler.assert_called_once()
882882

883+
def test_template_removal_fires_post_delete_signals(self):
884+
"""Regression test for #1221: removing a VPN template must trigger
885+
VpnClient.post_delete so that the peer cache is invalidated,
886+
certificates are revoked, and IP addresses are released."""
887+
device, vpn, template = self._create_wireguard_vpn_template()
888+
vpn_client = device.config.vpnclient_set.first()
889+
self.assertIsNotNone(vpn_client)
890+
cert = vpn_client.cert
891+
ip = vpn_client.ip
892+
self.assertIsNotNone(ip)
893+
initial_ip_count = IpAddress.objects.count()
894+
895+
with self.subTest("peer cache invalidated on template removal"):
896+
with catch_signal(vpn_peers_changed) as handler:
897+
device.config.templates.remove(template)
898+
handler.assert_called_once()
899+
900+
with self.subTest("VpnClient deleted"):
901+
self.assertEqual(device.config.vpnclient_set.count(), 0)
902+
903+
with self.subTest("IP address released"):
904+
self.assertLess(IpAddress.objects.count(), initial_ip_count)
905+
906+
with self.subTest("certificate revoked for OpenVPN-style auto_cert"):
907+
# For WireGuard there's no x509 cert to revoke, but the
908+
# post_delete handler should still have run without error.
909+
# The cert object should not exist anymore (CASCADE from VpnClient).
910+
self.assertFalse(
911+
VpnClient.objects.filter(pk=vpn_client.pk).exists()
912+
)
913+
914+
def test_deactivation_fires_post_delete_signals(self):
915+
"""Regression test for #1221: deactivating a device must trigger
916+
VpnClient.post_delete for each client (not bulk QuerySet.delete)."""
917+
device, vpn, template = self._create_wireguard_vpn_template()
918+
self.assertEqual(device.config.vpnclient_set.count(), 1)
919+
initial_ip_count = IpAddress.objects.count()
920+
921+
with catch_signal(vpn_peers_changed) as handler:
922+
device.config.templates.clear()
923+
# post_clear with deactivating state deletes vpn clients
924+
# The signal should fire because per-instance delete is used
925+
if device.config.vpnclient_set.count() == 0:
926+
handler.assert_called()
927+
928+
# Verify cleanup happened
929+
self.assertEqual(device.config.vpnclient_set.count(), 0)
930+
883931

884932
class TestVxlan(BaseTestVpn, TestVxlanWireguardVpnMixin, TestCase):
885933
def test_vxlan_config_creation(self):

0 commit comments

Comments
 (0)