-
-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
VPN template conflict during template switching #977
base: master
Are you sure you want to change the base?
Conversation
Resolves a bug where VPN configuration variables become unresolved when switching between VPN templates using the same VPN server
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shwetd19 kindly add a failing test for the bug described in the issue. A fix for this issue should not remove the existing features, i.e. the existing tests should not fail due to missing logic.
@@ -245,7 +245,6 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs): | |||
instance is using templates which have type set to "VPN" | |||
and "auto_cert" set to True. | |||
This method is called from a django signal (m2m_changed) | |||
see config.apps.ConfigConfig.connect_signals |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove the comments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pandafy Sorry for removing the comments - that was unintentional. I've restored the original docstring comments in my latest commit
for template in templates.filter(type='vpn'): | ||
if template.vpn_id not in current_vpns: | ||
instance.vpnclient_set.filter(vpn=template.vpn).delete() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be changed into
for template in templates.filter(type='vpn'): | |
if template.vpn_id not in current_vpns: | |
instance.vpnclient_set.filter(vpn=template.vpn).delete() | |
instance.vpnclient_set.exclude(vpn__in=current_vpns).delete() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to ensure that after the operation, there are not two VpnClients for this config object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pandafy Thanks for catching this! I understand the concern about potential duplicate VPN clients. I've modified the code to use your suggested approach:
-
In post_add:
- First create new VPN clients
- Then cleanup any unused clients with exclude(vpn__in=current_vpns)
-
In post_remove:
- Simply cleanup all unused clients with exclude(vpn__in=current_vpns)
This ensures we won't have duplicate VPN clients for the same config object while preserving the necessary clients. The full cleanup at the end handles any edge cases where duplicate clients might have been created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a test which verifies this.
- Kept the original docstring comments - Modified the VPN client cleanup logic as suggested - Fixed the formatting issues (blank lines with whitespace) - Added a failing test case for the bug
Hey @pandafy I've implemented the suggested changes, can you pls check once |
Hey @pandafy @nemesifier can you please reivew this PR ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shwetd19 add a test. You can take reference from this existing test here
openwisp-controller/openwisp_controller/config/tests/test_admin.py
Lines 1623 to 1704 in 129a42f
def test_vpn_clients_deleted(self): | |
def _update_template(templates): | |
params.update( | |
{ | |
'config-0-templates': ','.join( | |
[str(template.pk) for template in templates] | |
) | |
} | |
) | |
response = self.client.post(path, data=params, follow=True) | |
self.assertEqual(response.status_code, 200) | |
for template in templates: | |
self.assertContains( | |
response, f'class="sortedm2m" checked> {template.name}' | |
) | |
return response | |
vpn = self._create_vpn() | |
template = self._create_template() | |
vpn_template = self._create_template( | |
name='vpn-test', | |
type='vpn', | |
vpn=vpn, | |
auto_cert=True, | |
) | |
cert_query = Cert.objects.exclude(pk=vpn.cert_id) | |
valid_cert_query = cert_query.filter(revoked=False) | |
revoked_cert_query = cert_query.filter(revoked=True) | |
# Add a new device | |
path = reverse(f'admin:{self.app_label}_device_add') | |
params = self._get_device_params(org=self._get_org()) | |
response = self.client.post(path, data=params, follow=True) | |
self.assertEqual(response.status_code, 200) | |
config = Device.objects.get(name=params['name']).config | |
self.assertEqual(config.vpnclient_set.count(), 0) | |
self.assertEqual(config.templates.count(), 0) | |
path = reverse(f'admin:{self.app_label}_device_change', args=[config.device_id]) | |
params.update( | |
{ | |
'config-0-id': str(config.pk), | |
'config-0-device': str(config.device_id), | |
'config-INITIAL_FORMS': 1, | |
'_continue': True, | |
} | |
) | |
with self.subTest('Adding only VpnClient template'): | |
# Adding VpnClient template to the device | |
_update_template(templates=[vpn_template]) | |
self.assertEqual(config.templates.count(), 1) | |
self.assertEqual(config.vpnclient_set.count(), 1) | |
self.assertEqual(cert_query.count(), 1) | |
self.assertEqual(valid_cert_query.count(), 1) | |
# Remove VpnClient template from the device | |
_update_template(templates=[]) | |
self.assertEqual(config.templates.count(), 0) | |
self.assertEqual(config.vpnclient_set.count(), 0) | |
# Removing VPN template marks the related certificate as revoked | |
self.assertEqual(revoked_cert_query.count(), 1) | |
self.assertEqual(valid_cert_query.count(), 0) | |
with self.subTest('Add VpnClient template along with another template'): | |
# Adding templates to the device | |
_update_template(templates=[template, vpn_template]) | |
self.assertEqual(config.templates.count(), 2) | |
self.assertEqual(config.vpnclient_set.count(), 1) | |
self.assertEqual(valid_cert_query.count(), 1) | |
# Remove VpnClient template from the device | |
_update_template(templates=[template]) | |
self.assertEqual(config.templates.count(), 1) | |
self.assertEqual(config.vpnclient_set.count(), 0) | |
self.assertEqual(valid_cert_query.count(), 0) | |
self.assertEqual(revoked_cert_query.count(), 2) |
The test should do the following operations:
- Create a VPN object
- Create two templates with the same vpn server. (say template1 and template 2)
vpn = self._create_vpn()
template1 = self._create_template(
name='vpn-test-1',
type='vpn',
vpn=vpn,
config={},
auto_cert=True,
default=True, # This will auto-apply template1 to the device
)
template2 = self._create_template(
name='vpn-test-2',
type='vpn',
vpn=vpn,
config={},
auto_cert=True,
)
- Using the Django admin, apply tmplate2 and remove template1 from the device
- Verify there's only 1 VpnClient present for the config
- Verify that the variables are correctly resolved in the config using the following:
self.assertEqual(
config.backend_instance.config['openvpn'][0]['cert'],
f'/etc/x509/client-{vpn.pk.hex}.pem',
)
We cannot accept a bugfix patch with a test. Ensure that the test fails without the changes you've made in openwisp_controller/config/base/config.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @pandafy @nemesifier , Thank you for the clarification. I understand that I need to write a test that replicates the issue and fails without the patch. I’ll follow the structure you provided and ensure the test verifies the presence of only one Just to confirm, should the test also include a check for the behavior when switching back and forth between I’ll proceed with writing the test and will update the PR soon. If there’s anything else I should keep in mind, please let me know. Thanks again for your guidance! Best regards, This reply shows that you’re actively engaging with the feedback, seeking clarification to ensure you meet expectations, and demonstrating your commitment to addressing the issue properly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to confirm, should the test also include a check for the behavior when switching back and forth between
template1
andtemplate2
multiple times? Or is it sufficient to test a single switch fromtemplate1
totemplate2
?I’ll proceed with writing the test and will update the PR soon. If there’s anything else I should keep in mind, please let me know.
Switching back would from template2 to template1 would be nice and shouldn't involve a lot more effort.
Thanks again for your guidance!
Welcome!
PS: look at the existing tests to get inspiration on how to write your test, try to be consistent with the rest of the codebase please.
Hey @pandafy @nemesifier I've added the fix here as : Added a test to replicate the bug and verify the fix, which creates two VPN templates using the same VPN server, switches between them, and verifies that only one |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shwetd19 I don't see any progress on this PR after my last review.
Your recent commits have deleted the changes to the vpn_templates logic, kindly rectify it.
config.templates.add(template1) | ||
config.save() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have used default=True
while creating the template, thus we don't need to add the template1 to the Config object.
@@ -812,5 +812,63 @@ def manage_backend_changed(cls, instance_id, old_backend, backend, **kwargs): | |||
old_templates = device_group.templates.filter(backend=old_backend) | |||
config.manage_group_templates(templates, old_templates, not created) | |||
|
|||
def test_vpn_template_switch(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shwetd19 why did you add this test in the model definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shwetd19 have you verified that test_vpn_template_switch
fails with a clear error message without the code fix?
@@ -256,7 +256,7 @@ def manage_vpn_clients(cls, action, instance, pk_set, **kwargs): | |||
|
|||
if action == 'post_clear': | |||
if instance.is_deactivating_or_deactivated(): | |||
# If the device is deactivated or in the process of deactivating, then | |||
# If the device is deactivated or in the process of deactivatiing, then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# If the device is deactivated or in the process of deactivatiing, then | |
# If the device is deactivated or in the process of deactivating, then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PS: QA checks are failing.
Resolves a bug where VPN configuration variables become unresolved when switching between VPN templates using the same VPN server
Checklist
Reference to Existing Issue
Closes #973
Description of Changes
This PR fixes a race condition in the VPN client management code that occurs when switching between VPN templates that use the same VPN server. The bug popups when simultaneously removing one VPN template while adding another that uses the same VPN server.
The Problem
The issue was occuring due to how VPN clients are managed during template changes:
When adding a new template (
post_add
):When removing a template (
post_remove
):As a result, the configuration could end up with no VPN client object, causing VPN configuration variables to remain unresolved.
The Solution
The fix modifies the
manage_vpn_clients
method to:post_add
:post_remove
:This soln ensures VPN clients are preserved when switching between templates that use the same VPN server, maintaining proper configuration variable resolution.