Skip to content

Commit 874c6ee

Browse files
committed
[req-changes] Added first review changes
- Removed Zerotier read-only fields from the schema and utilized only the 'network_id' as the system-defined variable for the Zerotier network. - Fixed the issue of unwanted execution of "unsaved-changes.js" caused by the different order of configuration received from the API and the configuration defined in the VPN backend. - Enabled OpenWISP to control IP assignment using the subnet and IP fields of the VPN model. - Moved API requests for delete and update operations for Zerotier to background celery tasks, while the create operation remains synchronous. - Implemented basic error handling for Zerotier service API calls (further improvements will be made in the next review). - Updated existing migrations to include updated help text. - Fixed JS to hide unnecessary fields.
1 parent b2db3c7 commit 874c6ee

File tree

9 files changed

+184
-100
lines changed

9 files changed

+184
-100
lines changed

openwisp_controller/config/api/zerotier_service.py

+29-21
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ def _get_endpoint(self, property, operation, id):
1414
}
1515
return _API_ENDPOINTS.get(property).get(operation)
1616

17-
def __init__(self, host, token):
17+
def __init__(self, host, token, subnet='', ip=''):
1818
self.host = host
1919
self.token = token
20+
self.subnet = subnet and subnet.subnet
21+
self.ip_address = ip and ip.ip_address
2022
self.url = f'http://{host}'
2123
self.headers = {
2224
'X-ZT1-Auth': self.token,
@@ -32,19 +34,41 @@ def _get_repsonse(self, repsonse):
3234
'clientId',
3335
'rulesSource',
3436
'ssoEnabled',
37+
'creationTime',
38+
'name',
39+
'nwid',
40+
'objtype',
41+
'revision',
42+
'routes',
43+
'ipAssignmentPools',
3544
]
3645
for field in _redundant_fields:
3746
if field in repsonse.keys():
3847
del repsonse[field]
3948
return repsonse
4049

50+
def _add_routes_and_ip_assignment(self, config):
51+
config['routes'] = [{'target': str(self.subnet), 'via': self.ip_address}]
52+
try:
53+
ip_end = str(self.subnet[-2])
54+
ip_start = str(self.subnet[1])
55+
# In case of prefix length 32 (ipv4)
56+
# or 128 (ipv6) only single host is available
57+
except IndexError:
58+
ip_end = str(self.subnet[0])
59+
ip_start = str(self.subnet[0])
60+
61+
config['ipAssignmentPools'] = [{"ipRangeEnd": ip_end, "ipRangeStart": ip_start}]
62+
return config
63+
4164
def create_network(self, config):
4265
# Call /status first to obtain
4366
# the `node_id` of the controller
4467
url = f'{self.url}/status'
4568
response = requests.get(url, headers=self.headers)
4669
node_id = response.json()['address']
4770
url = f"{self.url}{self._get_endpoint('network', 'create', node_id)}"
71+
config = self._add_routes_and_ip_assignment(config)
4872
response = requests.post(url, json=config, headers=self.headers, timeout=5)
4973
if response.status_code != 200:
5074
raise ValidationError(
@@ -56,29 +80,13 @@ def create_network(self, config):
5680
)
5781
return self._get_repsonse(response.json())
5882

59-
def update_network(self, config):
60-
network_id = config.get('id')
83+
def update_network(self, config, network_id):
6184
url = f"{self.url}{self._get_endpoint('network', 'update', network_id)}"
85+
config = self._add_routes_and_ip_assignment(config)
6286
response = requests.post(url, json=config, headers=self.headers, timeout=5)
63-
if response.status_code != 200:
64-
raise ValidationError(
65-
{
66-
'ZerotierServiceAPI create network error': (
67-
f'({response.status_code}) {response.reason}'
68-
)
69-
}
70-
)
71-
return self._get_repsonse(response.json())
87+
return response, self._get_repsonse(response.json())
7288

7389
def delete_network(self, network_id):
7490
url = f"{self.url}{self._get_endpoint('network', 'delete', network_id)}"
75-
print(url)
7691
response = requests.delete(url, headers=self.headers)
77-
if response.status_code not in (200, 404):
78-
raise ValidationError(
79-
{
80-
'ZerotierServiceAPI delete network error': (
81-
f'({response.status_code}) {response.reason}'
82-
)
83-
}
84-
)
92+
return response

openwisp_controller/config/base/vpn.py

+80-46
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import logging
55
import subprocess
6+
from copy import deepcopy
67

78
import shortuuid
89
from cache_memoize import cache_memoize
@@ -20,7 +21,12 @@
2021
from .. import settings as app_settings
2122
from ..api.zerotier_service import ZerotierService
2223
from ..signals import vpn_peers_changed, vpn_server_modified
23-
from ..tasks import create_vpn_dh, trigger_vpn_server_endpoint
24+
from ..tasks import (
25+
create_vpn_dh,
26+
trigger_vpn_server_endpoint,
27+
trigger_zerotier_server_delete,
28+
trigger_zerotier_server_update,
29+
)
2430
from .base import BaseConfig
2531

2632
logger = logging.getLogger(__name__)
@@ -118,6 +124,8 @@ class AbstractVpn(ShareableOrgMixinUniqueName, BaseConfig):
118124
# needed for wireguard
119125
public_key = models.CharField(blank=True, max_length=44)
120126
private_key = models.CharField(blank=True, max_length=44)
127+
# needed for zerotier
128+
network_id = models.CharField(blank=True, max_length=16)
121129

122130
__vpn__ = True
123131

@@ -144,6 +152,7 @@ def clean(self, *args, **kwargs):
144152
self._validate_org_relation('cert')
145153
self._validate_org_relation('subnet')
146154
self._validate_subnet_ip()
155+
self._validate_authtoken()
147156

148157
def _validate_backend(self):
149158
if self._state.adding:
@@ -181,7 +190,7 @@ def _validate_keys(self):
181190
self.private_key = ''
182191

183192
def _validate_subnet_ip(self):
184-
if self._is_backend_type('wireguard'):
193+
if self._is_backend_type('wireguard') or self._is_backend_type('zerotier'):
185194
if not self.subnet:
186195
raise ValidationError(
187196
{'subnet': _('Subnet is required for this VPN backend.')}
@@ -191,6 +200,11 @@ def _validate_subnet_ip(self):
191200
{'ip': _('VPN IP address must be within the VPN subnet')}
192201
)
193202

203+
def _validate_authtoken(self):
204+
if self._is_backend_type('zerotier'):
205+
if not self.auth_token:
206+
raise ValidationError({'auth_token': _('Auth token is required')})
207+
194208
def save(self, *args, **kwargs):
195209
"""
196210
Calls _auto_create_cert() if cert is not set
@@ -199,33 +213,6 @@ def save(self, *args, **kwargs):
199213
if not created:
200214
self._check_changes()
201215
create_dh = False
202-
if self._is_backend_type('zerotier'):
203-
if not self.auth_token:
204-
raise ValidationError(
205-
{'auth_token': _('Zerotier auth token is required')}
206-
)
207-
208-
# The network name is similar
209-
# to the VPN backend's name field
210-
zerotier_config = self.config.get('zerotier')[0]
211-
zerotier_config['name'] = self.name
212-
213-
if zerotier_config and not zerotier_config.get('id'):
214-
response = ZerotierService(self.host, self.auth_token).create_network(
215-
self.config.get('zerotier')[0]
216-
)
217-
# Append newly created zerotier
218-
# controller network configuration
219-
self.config = {**self.config, 'zerotier': [response]}
220-
221-
# When zerotier network already exists
222-
if zerotier_config and zerotier_config.get('id'):
223-
response = ZerotierService(self.host, self.auth_token).update_network(
224-
self.config.get('zerotier')[0]
225-
)
226-
# update the existing network configuration
227-
self.config = {**self.config, 'zerotier': [response]}
228-
229216
if not self.cert and self.ca:
230217
self.cert = self._auto_create_cert()
231218
if self._is_backend_type('openvpn') and not self.dh:
@@ -235,6 +222,13 @@ def save(self, *args, **kwargs):
235222
self._generate_wireguard_keys()
236223
if self.subnet and not self.ip:
237224
self.ip = self._auto_create_ip()
225+
if self._is_backend_type('zerotier'):
226+
config = deepcopy(self.config['zerotier'][0])
227+
config['name'] = self.name
228+
if created:
229+
self._create_zerotier_server(config)
230+
else:
231+
self._update_zerotier_server(config)
238232
super().save(*args, **kwargs)
239233
if create_dh:
240234
transaction.on_commit(lambda: create_vpn_dh.delay(self.id))
@@ -256,6 +250,7 @@ def _check_changes(self):
256250
'dh',
257251
'public_key',
258252
'private_key',
253+
'network_id',
259254
]
260255
current = self._meta.model.objects.only(*attrs).get(pk=self.pk)
261256
for attr in attrs:
@@ -284,25 +279,53 @@ class method for ``post_delete`` signal
284279
"""
285280
if not instance._is_backend_type('zerotier'):
286281
return
287-
network_id = instance.config.get('zerotier')[0].get('id')
288-
ZerotierService(instance.host, instance.auth_token).delete_network(network_id)
282+
transaction.on_commit(
283+
lambda: trigger_zerotier_server_delete.delay(
284+
host=instance.host,
285+
auth_token=instance.auth_token,
286+
network_id=instance.network_id,
287+
vpn_id=instance.pk,
288+
)
289+
)
290+
291+
def _create_zerotier_server(self, config):
292+
server_config = ZerotierService(
293+
self.host, self.auth_token, self.subnet, self.ip
294+
).create_network(config)
295+
self.network_id = server_config.pop('id', None)
296+
self.config = {**self.config, 'zerotier': [server_config]}
297+
298+
def _update_zerotier_server(self, config):
299+
config = config or {}
300+
if config and self._is_backend_type('zerotier'):
301+
if self.host and self.auth_token:
302+
transaction.on_commit(
303+
lambda: trigger_zerotier_server_update.delay(
304+
config=config,
305+
vpn_id=self.pk,
306+
)
307+
)
308+
else:
309+
logger.info(
310+
f'Cannot update configuration of {self.name}'
311+
'Zerotier VPN server, host and authentication token are empty.'
312+
)
289313

290314
def update_vpn_server_configuration(instance, **kwargs):
291-
if not instance._is_backend_type('wireguard'):
292-
return
293-
if instance.webhook_endpoint and instance.auth_token:
294-
transaction.on_commit(
295-
lambda: trigger_vpn_server_endpoint.delay(
296-
endpoint=instance.webhook_endpoint,
297-
auth_token=instance.auth_token,
298-
vpn_id=instance.pk,
315+
if instance._is_backend_type('wireguard'):
316+
if instance.webhook_endpoint and instance.auth_token:
317+
transaction.on_commit(
318+
lambda: trigger_vpn_server_endpoint.delay(
319+
endpoint=instance.webhook_endpoint,
320+
auth_token=instance.auth_token,
321+
vpn_id=instance.pk,
322+
)
323+
)
324+
else:
325+
logger.info(
326+
f'Cannot update configuration of {instance.name} VPN server, '
327+
'webhook endpoint and authentication token are empty.'
299328
)
300-
)
301-
else:
302-
logger.info(
303-
f'Cannot update configuration of {instance.name} VPN server, '
304-
'webhook endpoint and authentication token are empty.'
305-
)
306329

307330
def _auto_create_cert(self):
308331
"""
@@ -360,6 +383,8 @@ def get_context(self):
360383
c['subnet_prefixlen'] = str(self.subnet.subnet.prefixlen)
361384
if self.ip:
362385
c['ip_address'] = self.ip.ip_address
386+
if self.network_id:
387+
c['network_id'] = self.network_id
363388
c.update(sorted(super().get_context().items()))
364389
return c
365390

@@ -391,6 +416,9 @@ def get_vpn_server_context(self):
391416
context_keys['server_ip_network']
392417
] = f'{self.ip.ip_address}/{self.subnet.subnet.max_prefixlen}'
393418
context[context_keys['vpn_subnet']] = str(self.subnet.subnet)
419+
if self._is_backend_type('zerotier') and self.network_id:
420+
context[context_keys['network_name']] = self.name
421+
context[context_keys['network_id']] = self.network_id
394422
return context
395423

396424
def get_system_context(self):
@@ -422,6 +450,8 @@ def _get_auto_context_keys(self):
422450
* ip address
423451
VXLAN:
424452
* vni (VXLAN Network Identifier)
453+
ZeroTier
454+
* network_id (ZeroTier Network Identifier)
425455
"""
426456
pk = self.pk.hex
427457
context_keys = {
@@ -457,6 +487,9 @@ def _get_auto_context_keys(self):
457487
'server_ip_network': 'server_ip_network_{}'.format(pk),
458488
}
459489
)
490+
if self._is_backend_type('zerotier'):
491+
context_keys.update({'network_id': 'network_id_{}'.format(pk)})
492+
context_keys.update({'network_name': 'network_name_{}'.format(pk)})
460493
return context_keys
461494

462495
def auto_client(self, auto_cert=True, template_backend_class=None):
@@ -492,7 +525,8 @@ def auto_client(self, auto_cert=True, template_backend_class=None):
492525
# call auto_client and update the config
493526
elif self._is_backend_type('zerotier') and template_backend_class:
494527
auto = backend.auto_client(
495-
host=self.host,
528+
name=self.name,
529+
nwid=self.network_id,
496530
server=self.config[config_dict_key][0],
497531
**context_keys,
498532
)

openwisp_controller/config/migrations/0039_wireguard_vxlan_ipam.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ class Migration(migrations.Migration):
4646
name='auth_token',
4747
field=models.CharField(
4848
blank=True,
49-
help_text=('Authentication token for triggering "Webhook Endpoint"'),
49+
help_text=(
50+
'Authentication token used for triggering '
51+
'"Webhook Endpoint" or for calling "ZerotierService" API'
52+
),
5053
max_length=128,
5154
null=True,
5255
verbose_name='Webhook AuthToken',

openwisp_controller/config/migrations/0048_alter_vpn_auth_token.py

-27
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.19 on 2023-06-26 12:51
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('config', '0047_add_organizationlimits'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='vpn',
15+
name='network_id',
16+
field=models.CharField(blank=True, max_length=16),
17+
),
18+
]

openwisp_controller/config/static/config/js/vpn.js

-5
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ django.jQuery(function ($) {
5050
// For Zerotier VPN backend
5151
if(backendValue.includes('zerotier')){
5252
$('label[for="id_auth_token"]').parent().parent().show();
53-
$('label[for="id_subnet"]').parent().parent().hide();
54-
$('label[for="id_ip"]').parent().parent().hide();
55-
} else {
56-
$('label[for="id_subnet"]').parent().parent().show();
57-
$('label[for="id_ip"]').parent().parent().show();
5853
}
5954
};
6055

0 commit comments

Comments
 (0)