From 116c0536fc4fca2ad677288ae1bd1d4983f97aa6 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Tue, 30 May 2023 19:24:35 +0530 Subject: [PATCH 1/3] [feature] Added ZeroTier backend #208 Closes #208 --- README.rst | 1 + netjsonconfig/__init__.py | 2 + netjsonconfig/backends/zerotier/__init__.py | 0 netjsonconfig/backends/zerotier/converters.py | 20 +++++ netjsonconfig/backends/zerotier/parser.py | 21 +++++ netjsonconfig/backends/zerotier/renderer.py | 15 ++++ netjsonconfig/backends/zerotier/schema.py | 68 +++++++++++++++++ .../zerotier/templates/zerotier.jinja2 | 17 +++++ netjsonconfig/backends/zerotier/zerotier.py | 26 +++++++ tests/zerotier/__init__.py | 0 tests/zerotier/test_backend.py | 76 +++++++++++++++++++ tests/zerotier/test_parser.py | 34 +++++++++ 12 files changed, 280 insertions(+) create mode 100644 netjsonconfig/backends/zerotier/__init__.py create mode 100644 netjsonconfig/backends/zerotier/converters.py create mode 100644 netjsonconfig/backends/zerotier/parser.py create mode 100644 netjsonconfig/backends/zerotier/renderer.py create mode 100644 netjsonconfig/backends/zerotier/schema.py create mode 100644 netjsonconfig/backends/zerotier/templates/zerotier.jinja2 create mode 100644 netjsonconfig/backends/zerotier/zerotier.py create mode 100644 tests/zerotier/__init__.py create mode 100644 tests/zerotier/test_backend.py create mode 100644 tests/zerotier/test_parser.py diff --git a/README.rst b/README.rst index 8a3c09dc3..5bef45198 100644 --- a/README.rst +++ b/README.rst @@ -44,6 +44,7 @@ Its main features are listed below for your reference: * `OpenWisp Firmware `_ support * `OpenVPN `_ support * `WireGuard `_ support +* `ZeroTier `_ support * Possibility to support more firmwares via custom backends * Based on the `NetJSON RFC `_ * **Validation** based on `JSON-Schema `_ diff --git a/netjsonconfig/__init__.py b/netjsonconfig/__init__.py index 1ba605d3a..84295410a 100644 --- a/netjsonconfig/__init__.py +++ b/netjsonconfig/__init__.py @@ -7,6 +7,7 @@ from .backends.openwrt.openwrt import OpenWrt # noqa from .backends.vxlan.vxlan_wireguard import VxlanWireguard # noqa from .backends.wireguard.wireguard import Wireguard # noqa +from .backends.zerotier.zerotier import ZeroTier # noqa from .version import VERSION, __version__, get_version # noqa @@ -16,6 +17,7 @@ def get_backends(): 'openwisp': OpenWisp, 'openvpn': OpenVpn, 'wireguard': Wireguard, + 'zerotier': ZeroTier, } logger = logging.getLogger(__name__) diff --git a/netjsonconfig/backends/zerotier/__init__.py b/netjsonconfig/backends/zerotier/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netjsonconfig/backends/zerotier/converters.py b/netjsonconfig/backends/zerotier/converters.py new file mode 100644 index 000000000..1c5a83047 --- /dev/null +++ b/netjsonconfig/backends/zerotier/converters.py @@ -0,0 +1,20 @@ +from ..base.converter import BaseConverter +from .schema import schema + + +class ZeroTier(BaseConverter): + netjson_key = 'zerotier' + intermediate_key = 'zerotier' + _schema = schema + + def to_intermediate_loop(self, block, result, index=None): + vpn = self.__intermediate_vpn(block) + result.setdefault('zerotier', []) + result['zerotier'].append(vpn) + return result + + def __intermediate_vpn(self, config, remove=[False, 0, '']): + # add zerotier network id and name + config['nwid'] = config.pop('id') + config['n'] = config.get('name') + return self.sorted_dict(config) diff --git a/netjsonconfig/backends/zerotier/parser.py b/netjsonconfig/backends/zerotier/parser.py new file mode 100644 index 000000000..1a76ae765 --- /dev/null +++ b/netjsonconfig/backends/zerotier/parser.py @@ -0,0 +1,21 @@ +import re + +from ..base.parser import BaseParser + +vpn_pattern = re.compile('^# zerotier config:\s', flags=re.MULTILINE) +config_pattern = re.compile('^([^\s]*) ?(.*)$') +config_suffix = '.conf' + + +class ZeroTierParser(BaseParser): + def parse_text(self, config): + raise NotImplementedError() + + def parse_tar(self, tar): + raise NotImplementedError() + + def _get_vpns(self, text): + raise NotImplementedError() + + def _get_config(self, contents): + raise NotImplementedError() diff --git a/netjsonconfig/backends/zerotier/renderer.py b/netjsonconfig/backends/zerotier/renderer.py new file mode 100644 index 000000000..9d88e1daf --- /dev/null +++ b/netjsonconfig/backends/zerotier/renderer.py @@ -0,0 +1,15 @@ +from ..base.renderer import BaseRenderer + + +class ZeroTierRenderer(BaseRenderer): + """ + ZeroTier Renderer + """ + + def cleanup(self, output): + # remove indentations + output = output.replace(' ', '') + # remove last newline + if output.endswith('\n\n'): + output = output[0:-1] + return output diff --git a/netjsonconfig/backends/zerotier/schema.py b/netjsonconfig/backends/zerotier/schema.py new file mode 100644 index 000000000..3fd2acab4 --- /dev/null +++ b/netjsonconfig/backends/zerotier/schema.py @@ -0,0 +1,68 @@ +""" +ZeroTier specific JSON-Schema definition +""" + +from copy import deepcopy + +from ...schema import schema as default_schema + +# The schema is taken from OpenAPI specification: +# https://docs.zerotier.com/service/v1/ (self-hosted controllers) +# https://docs.zerotier.com/openapi/centralv1.json (central controllers) +base_zerotier_schema = { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": True, + "properties": { + "zerotier": { + "type": "array", + "title": "ZeroTier", + "uniqueItems": True, + "additionalItems": True, + "propertyOrder": 12, + "items": { + "type": "object", + "title": "ZeroTier", + "additionalProperties": True, + "required": ["name", "id"], + "properties": { + "id": { + "title": "network id", + "description": "ZeroTier network ID", + "type": "string", + "minLength": 16, + "maxLength": 16, + "propertyOrder": 1, + }, + "nwid": { + "title": "network id (legacy field)", + "description": "ZeroTier network ID (legacy field)", + "type": "string", + "minLength": 16, + "maxLength": 16, + "propertyOrder": 2, + }, + "name": { + "title": "network name", + "description": "ZeroTier network name", + "type": "string", + "propertyOrder": 3, + }, + "private": { + "title": "private", + "type": "boolean", + "description": ( + "Whether or not the zerotier network is private." + "If false, members will NOT need to be authorized to join.", + ), + "propertyOrder": 4, + }, + }, + }, + } + }, +} + +schema = deepcopy(base_zerotier_schema) +schema['required'] = ['zerotier'] +schema['properties']['files'] = default_schema['properties']['files'] diff --git a/netjsonconfig/backends/zerotier/templates/zerotier.jinja2 b/netjsonconfig/backends/zerotier/templates/zerotier.jinja2 new file mode 100644 index 000000000..6a7a50dde --- /dev/null +++ b/netjsonconfig/backends/zerotier/templates/zerotier.jinja2 @@ -0,0 +1,17 @@ +{% for vpn in data.zerotier %} +# zerotier config: {{ vpn.nwid }} + + {% for key, value in vpn.items() %} + {% set key = key.replace('_', '-') %} + {% if key != 'name' %} + {% if value is string or value is number %} + {{ key }}={{ value }} + {% elif value is iterable %} + {% for element in value %} + {{ key }}={{ element }} + {% endfor %} + {% endif %} + {% endif %} + {% endfor %} + +{% endfor %} diff --git a/netjsonconfig/backends/zerotier/zerotier.py b/netjsonconfig/backends/zerotier/zerotier.py new file mode 100644 index 000000000..f8e9ff765 --- /dev/null +++ b/netjsonconfig/backends/zerotier/zerotier.py @@ -0,0 +1,26 @@ +from ..base.backend import BaseVpnBackend +from . import converters +from .parser import config_suffix, vpn_pattern +from .renderer import ZeroTierRenderer +from .schema import schema + + +class ZeroTier(BaseVpnBackend): + schema = schema + converters = [converters.ZeroTier] + renderer = ZeroTierRenderer + # BaseVpnBackend attributes + vpn_pattern = vpn_pattern + config_suffix = config_suffix + + @classmethod + def auto_client(cls, server={}, **kwargs): + network_id = server.get('id', server.get('nwid', '')) + return { + 'zerotier': [ + { + 'id': network_id, + 'name': server.get('name', ''), + } + ] + } diff --git a/tests/zerotier/__init__.py b/tests/zerotier/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/zerotier/test_backend.py b/tests/zerotier/test_backend.py new file mode 100644 index 000000000..ab2f28ac5 --- /dev/null +++ b/tests/zerotier/test_backend.py @@ -0,0 +1,76 @@ +import tarfile +import unittest + +from netjsonconfig import ZeroTier +from netjsonconfig.exceptions import ValidationError + + +class TestBackend(unittest.TestCase): + """ + Tests for ZeroTier backend + """ + + maxDiff = None + _TEST_CONFIG = { + "zerotier": [ + { + "id": "79vgjhks7ae448c5", + "name": "network-network-1", + "private": True, + }, + { + "id": "yt6c2e21c0fhhtyu", + "name": "zerotier-network-2", + "private": False, + }, + ] + } + + def test_test_schema(self): + with self.assertRaises(ValidationError) as context_manager: + ZeroTier({}).validate() + self.assertIn( + "'zerotier' is a required property", str(context_manager.exception) + ) + + def test_confs(self): + c = ZeroTier(self._TEST_CONFIG) + expected = """# zerotier config: 79vgjhks7ae448c5 + +n=network-network-1 +nwid=79vgjhks7ae448c5 +private=True + +# zerotier config: yt6c2e21c0fhhtyu + +n=zerotier-network-2 +nwid=yt6c2e21c0fhhtyu +private=False +""" + self.assertEqual(c.render(), expected) + + def test_generate(self): + c = ZeroTier(self._TEST_CONFIG) + tar = tarfile.open(fileobj=c.generate(), mode="r") + # tar object should contain both zerotier configuration + self.assertEqual(len(tar.getmembers()), 2) + vpn1 = tar.getmember("79vgjhks7ae448c5.conf") + contents = tar.extractfile(vpn1).read().decode() + expected = """n=network-network-1 +nwid=79vgjhks7ae448c5 +private=True +""" + self.assertEqual(contents, expected) + + def test_auto_client(self): + expected = { + "zerotier": [ + { + "id": "79vgjhks7ae448c5", + "name": "network-network-1", + } + ] + } + self.assertEqual( + ZeroTier.auto_client(server=self._TEST_CONFIG["zerotier"][0]), expected + ) diff --git a/tests/zerotier/test_parser.py b/tests/zerotier/test_parser.py new file mode 100644 index 000000000..72d8ee385 --- /dev/null +++ b/tests/zerotier/test_parser.py @@ -0,0 +1,34 @@ +import unittest +from unittest.mock import patch + +from netjsonconfig.backends.zerotier.parser import ZeroTierParser + + +class TestBaseParser(unittest.TestCase): + """ + Tests for netjsonconfig.backends.zerotier.parser.BaseParser + """ + + def test_parse_text(self): + # Creating an instance of ZeroTier Parser will raise + # NotImplementedError since it will requires "parse_text" + with self.assertRaises(NotImplementedError): + ZeroTierParser(config="") + + @patch.object(ZeroTierParser, "parse_text", return_value=None) + def test_parse_tar(self, mocked): + parser = ZeroTierParser(config="") + with self.assertRaises(NotImplementedError): + parser.parse_tar(tar=None) + + @patch.object(ZeroTierParser, "parse_text", return_value=None) + def test_get_vpns(self, mocked): + parser = ZeroTierParser(config="") + with self.assertRaises(NotImplementedError): + parser._get_vpns(text=None) + + @patch.object(ZeroTierParser, "parse_text", return_value=None) + def test_get_config(self, mocked): + parser = ZeroTierParser(config="") + with self.assertRaises(NotImplementedError): + parser._get_config(contents=None) From 62703341883fca8c18d5b4f57908b113c4e8c032 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Wed, 31 May 2023 20:15:48 +0530 Subject: [PATCH 2/3] [changes] Extended ZeroTier backend schema The schema is taken from OpenAPI specification: https://docs.zerotier.com/service/v1/ (self-hosted controllers) https://docs.zerotier.com/openapi/centralv1.json (central controllers) The Zerotier network configuration keys taken from: https://github.com/zerotier/ZeroTierOne/blob/dev/node/NetworkConfig.hpp --- netjsonconfig/backends/zerotier/converters.py | 22 ++- netjsonconfig/backends/zerotier/schema.py | 171 ++++++++++++++++-- tests/zerotier/test_backend.py | 126 ++++++++++--- 3 files changed, 269 insertions(+), 50 deletions(-) diff --git a/netjsonconfig/backends/zerotier/converters.py b/netjsonconfig/backends/zerotier/converters.py index 1c5a83047..fdd0d9d06 100644 --- a/netjsonconfig/backends/zerotier/converters.py +++ b/netjsonconfig/backends/zerotier/converters.py @@ -14,7 +14,25 @@ def to_intermediate_loop(self, block, result, index=None): return result def __intermediate_vpn(self, config, remove=[False, 0, '']): - # add zerotier network id and name + # The Zerotier network configuration keys taken from: + # https://github.com/zerotier/ZeroTierOne/blob/dev/node/NetworkConfig.hpp config['nwid'] = config.pop('id') - config['n'] = config.get('name') + config['t'] = config.pop('objtype') + config['r'] = config.pop('revision') + config['ts'] = config.pop('creationTime') + config['n'] = config.pop('name') + config['p'] = config.pop('private') + config['eb'] = config.pop('enableBroadcast') + config['v4s'] = config.pop('v4AssignMode') + config['v6s'] = config.pop('v6AssignMode') + config['mtu'] = config.pop('mtu') + config['ml'] = config.pop('multicastLimit') + config['I'] = config.pop('ipAssignmentPools') + config['RT'] = config.pop('routes') + config['DNS'] = config.pop('dns') + config['R'] = config.pop('rules') + config['CAP'] = config.pop('capabilities') + config['TAG'] = config.pop('tags') + config['tt'] = config.pop('remoteTraceTarget') + config['tl'] = config.pop('remoteTraceLevel') return self.sorted_dict(config) diff --git a/netjsonconfig/backends/zerotier/schema.py b/netjsonconfig/backends/zerotier/schema.py index 3fd2acab4..908534045 100644 --- a/netjsonconfig/backends/zerotier/schema.py +++ b/netjsonconfig/backends/zerotier/schema.py @@ -24,42 +24,175 @@ "type": "object", "title": "ZeroTier", "additionalProperties": True, - "required": ["name", "id"], + "required": ["name"], "properties": { + # Read-only properties "id": { - "title": "network id", - "description": "ZeroTier network ID", "type": "string", - "minLength": 16, - "maxLength": 16, - "propertyOrder": 1, + "readOnly": True, + "example": "3e245e31af000001", + "description": "Network ID", }, "nwid": { - "title": "network id (legacy field)", - "description": "ZeroTier network ID (legacy field)", "type": "string", - "minLength": 16, - "maxLength": 16, - "propertyOrder": 2, + "readOnly": True, + "example": "3e245e31af000001", + "description": "Network ID legacy field (same as 'id')", }, + "objtype": { + "type": "string", + "readOnly": True, + "example": "network", + }, + "revision": { + "type": "integer", + "example": 1, + "readOnly": True, + "description": "The revision number of the network configuration", + }, + "creationTime": { + "type": "number", + "readOnly": True, + "example": 1623101592, + "description": "Time the network was created", + }, + # Configurable properties "name": { - "title": "network name", - "description": "ZeroTier network name", "type": "string", - "propertyOrder": 3, + "example": "openwisp-wifi-network", + "description": "Name of the network", }, "private": { - "title": "private", "type": "boolean", "description": ( - "Whether or not the zerotier network is private." - "If false, members will NOT need to be authorized to join.", + "Whether or not the network is private." + "If false, members will *NOT* need to be authorized to join." ), - "propertyOrder": 4, + }, + "enableBroadcast": { + "type": "boolean", + "description": "Enable broadcast packets on the network", + }, + "v4AssignMode": { + "type": "object", + "properties": { + "zt": {"type": "boolean"}, + "description": "Whether ZeroTier should assign IPv4 addresses to members", + }, + }, + "v6AssignMode": { + "type": "object", + "properties": { + "6plane": { + "type": "boolean", + "description": "Whether 6PLANE addressing should be used for IPv6 assignment", + }, + "rfc4193": { + "type": "boolean", + "description": "Whether RFC4193 addressing should be used for IPv6 assignment", # noqa + }, + "zt": { + "type": "boolean", + "description": "Whether ZeroTier should assign IPv6 addresses to members", + }, + }, + }, + "mtu": { + "type": "integer", + "example": 2800, + "description": "MTU to set on the client virtual network adapter", + }, + "multicastLimit": { + "type": "integer", + "example": 32, + "description": ( + "Maximum number of recipients per multicast or broadcast." + "Warning - Setting this to 0 will disable IPv4 communication on your network!" + ), + }, + "routes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "target": { + "type": "string", + "example": "192.168.192.0/24", + "description": "The target IP address range for the route", + }, + "via": { + "type": "string", + "example": "192.168.192.1", + "description": "The IP address of the next hop for the route", + }, + }, + }, + "description": "Array of route objects", + }, + "ipAssignmentPools": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ipRangeStart": { + "type": "string", + "example": "192.168.192.1", + "description": "The starting IP address of the pool range", + }, + "ipRangeEnd": { + "type": "string", + "example": "192.168.192.254", + "description": "The ending IP address of the pool range", + }, + }, + }, + "description": "Range of IP addresses for the auto assign pool", + }, + "dns": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "example": "zerotier.openwisp.io", + "description": "The domain for DNS resolution", + }, + "servers": { + "type": "array", + "items": { + "type": "string", + "example": "10.147.20.3", + "description": "The DNS server IP addresses", + }, + }, + }, + }, + "rules": { + "type": "array", + "items": {"type": "object"}, + "description": "Array of network rule objects", + }, + "capabilities": { + "type": "array", + "items": {"type": "object"}, + "description": "Array of network capabilities", + }, + "tags": { + "type": "array", + "items": {"type": "object"}, + "description": "Array of network tag objects", + }, + "remoteTraceTarget": { + "type": "string", + "example": "7f5d90eb87", + "description": "The remote target ID for network tracing", + }, + "remoteTraceLevel": { + "type": "integer", + "description": "The level of network tracing", }, }, }, - } + }, }, } diff --git a/tests/zerotier/test_backend.py b/tests/zerotier/test_backend.py index ab2f28ac5..765b5906e 100644 --- a/tests/zerotier/test_backend.py +++ b/tests/zerotier/test_backend.py @@ -14,15 +14,50 @@ class TestBackend(unittest.TestCase): _TEST_CONFIG = { "zerotier": [ { - "id": "79vgjhks7ae448c5", - "name": "network-network-1", + "id": "9536600adf0af076", + "nwid": "9536600adf0af076", + "objtype": "network", + "revision": 1, + "creationTime": 1632012345, + "name": "zerotier-openwisp-network", "private": True, - }, - { - "id": "yt6c2e21c0fhhtyu", - "name": "zerotier-network-2", - "private": False, - }, + "enableBroadcast": True, + "v4AssignMode": {"zt": True}, + "v6AssignMode": {"6plane": False, "rfc4193": True, "zt": True}, + "mtu": 2700, + "multicastLimit": 16, + "routes": [{"target": "10.0.0.0/24", "via": "10.0.0.1"}], + "ipAssignmentPools": [ + {"ipRangeStart": "10.0.0.10", "ipRangeEnd": "10.0.0.100"} + ], + "dns": {"domain": "zerotier.openwisp.io", "servers": ["10.147.20.3"]}, + "rules": [ + { + "etherType": 2048, + "not": True, + "or": False, + "type": "MATCH_ETHERTYPE", + }, + {"type": "ACTION_DROP"}, + ], + "capabilities": [ + { + "default": True, + "id": 1, + "rules": [ + { + "etherType": 2048, + "not": True, + "or": False, + "type": "MATCH_ETHERTYPE", + } + ], + } + ], + "tags": [{"default": 1, "id": 1}], + "remoteTraceTarget": "7f5d90eb87", + "remoteTraceLevel": 1, + } ] } @@ -35,39 +70,72 @@ def test_test_schema(self): def test_confs(self): c = ZeroTier(self._TEST_CONFIG) - expected = """# zerotier config: 79vgjhks7ae448c5 - -n=network-network-1 -nwid=79vgjhks7ae448c5 -private=True - -# zerotier config: yt6c2e21c0fhhtyu + expected = """# zerotier config: 9536600adf0af076 -n=zerotier-network-2 -nwid=yt6c2e21c0fhhtyu -private=False -""" +CAP={'default': True, 'id': 1, 'rules': [{'etherType': 2048, 'not': True, 'or': False, 'type': 'MATCH_ETHERTYPE'}]} +DNS=domain +DNS=servers +I={'ipRangeStart': '10.0.0.10', 'ipRangeEnd': '10.0.0.100'} +R={'etherType': 2048, 'not': True, 'or': False, 'type': 'MATCH_ETHERTYPE'} +R={'type': 'ACTION_DROP'} +RT={'target': '10.0.0.0/24', 'via': '10.0.0.1'} +TAG={'default': 1, 'id': 1} +eb=True +ml=16 +mtu=2700 +n=zerotier-openwisp-network +nwid=9536600adf0af076 +p=True +r=1 +t=network +tl=1 +ts=1632012345 +tt=7f5d90eb87 +v4s=zt +v6s=6plane +v6s=rfc4193 +v6s=zt +""" # noqa self.assertEqual(c.render(), expected) def test_generate(self): c = ZeroTier(self._TEST_CONFIG) + expected = """CAP={'default': True, 'id': 1, 'rules': [{'etherType': 2048, 'not': True, 'or': False, 'type': 'MATCH_ETHERTYPE'}]} +DNS=domain +DNS=servers +I={'ipRangeStart': '10.0.0.10', 'ipRangeEnd': '10.0.0.100'} +R={'etherType': 2048, 'not': True, 'or': False, 'type': 'MATCH_ETHERTYPE'} +R={'type': 'ACTION_DROP'} +RT={'target': '10.0.0.0/24', 'via': '10.0.0.1'} +TAG={'default': 1, 'id': 1} +eb=True +ml=16 +mtu=2700 +n=zerotier-openwisp-network +nwid=9536600adf0af076 +p=True +r=1 +t=network +tl=1 +ts=1632012345 +tt=7f5d90eb87 +v4s=zt +v6s=6plane +v6s=rfc4193 +v6s=zt +""" # noqa tar = tarfile.open(fileobj=c.generate(), mode="r") - # tar object should contain both zerotier configuration - self.assertEqual(len(tar.getmembers()), 2) - vpn1 = tar.getmember("79vgjhks7ae448c5.conf") - contents = tar.extractfile(vpn1).read().decode() - expected = """n=network-network-1 -nwid=79vgjhks7ae448c5 -private=True -""" + self.assertEqual(len(tar.getmembers()), 1) + vpn = tar.getmember("9536600adf0af076.conf") + contents = tar.extractfile(vpn).read().decode() self.assertEqual(contents, expected) def test_auto_client(self): expected = { "zerotier": [ { - "id": "79vgjhks7ae448c5", - "name": "network-network-1", + "id": "9536600adf0af076", + "name": "zerotier-openwisp-network", } ] } From 15ae24e0e6f2d958a4d1f8a11f6f2a7c1a6214c2 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Mon, 5 Jun 2023 12:53:31 +0530 Subject: [PATCH 3/3] [changes] Rendered ZeroTier configuration in JSON format --- netjsonconfig/backends/zerotier/converters.py | 21 -- netjsonconfig/backends/zerotier/parser.py | 4 +- .../zerotier/templates/zerotier.jinja2 | 15 +- tests/zerotier/test_backend.py | 277 ++++++++++++++---- 4 files changed, 226 insertions(+), 91 deletions(-) diff --git a/netjsonconfig/backends/zerotier/converters.py b/netjsonconfig/backends/zerotier/converters.py index fdd0d9d06..3cd85fc3a 100644 --- a/netjsonconfig/backends/zerotier/converters.py +++ b/netjsonconfig/backends/zerotier/converters.py @@ -14,25 +14,4 @@ def to_intermediate_loop(self, block, result, index=None): return result def __intermediate_vpn(self, config, remove=[False, 0, '']): - # The Zerotier network configuration keys taken from: - # https://github.com/zerotier/ZeroTierOne/blob/dev/node/NetworkConfig.hpp - config['nwid'] = config.pop('id') - config['t'] = config.pop('objtype') - config['r'] = config.pop('revision') - config['ts'] = config.pop('creationTime') - config['n'] = config.pop('name') - config['p'] = config.pop('private') - config['eb'] = config.pop('enableBroadcast') - config['v4s'] = config.pop('v4AssignMode') - config['v6s'] = config.pop('v6AssignMode') - config['mtu'] = config.pop('mtu') - config['ml'] = config.pop('multicastLimit') - config['I'] = config.pop('ipAssignmentPools') - config['RT'] = config.pop('routes') - config['DNS'] = config.pop('dns') - config['R'] = config.pop('rules') - config['CAP'] = config.pop('capabilities') - config['TAG'] = config.pop('tags') - config['tt'] = config.pop('remoteTraceTarget') - config['tl'] = config.pop('remoteTraceLevel') return self.sorted_dict(config) diff --git a/netjsonconfig/backends/zerotier/parser.py b/netjsonconfig/backends/zerotier/parser.py index 1a76ae765..9395854a0 100644 --- a/netjsonconfig/backends/zerotier/parser.py +++ b/netjsonconfig/backends/zerotier/parser.py @@ -2,9 +2,9 @@ from ..base.parser import BaseParser -vpn_pattern = re.compile('^# zerotier config:\s', flags=re.MULTILINE) +vpn_pattern = re.compile('^// zerotier config:\s', flags=re.MULTILINE) config_pattern = re.compile('^([^\s]*) ?(.*)$') -config_suffix = '.conf' +config_suffix = '.json' class ZeroTierParser(BaseParser): diff --git a/netjsonconfig/backends/zerotier/templates/zerotier.jinja2 b/netjsonconfig/backends/zerotier/templates/zerotier.jinja2 index 6a7a50dde..e8e25594f 100644 --- a/netjsonconfig/backends/zerotier/templates/zerotier.jinja2 +++ b/netjsonconfig/backends/zerotier/templates/zerotier.jinja2 @@ -1,17 +1,6 @@ {% for vpn in data.zerotier %} -# zerotier config: {{ vpn.nwid }} +// zerotier config: {{ vpn.nwid }} - {% for key, value in vpn.items() %} - {% set key = key.replace('_', '-') %} - {% if key != 'name' %} - {% if value is string or value is number %} - {{ key }}={{ value }} - {% elif value is iterable %} - {% for element in value %} - {{ key }}={{ element }} - {% endfor %} - {% endif %} - {% endif %} - {% endfor %} +{{ vpn | tojson(indent=2)}} {% endfor %} diff --git a/tests/zerotier/test_backend.py b/tests/zerotier/test_backend.py index 765b5906e..286f262ac 100644 --- a/tests/zerotier/test_backend.py +++ b/tests/zerotier/test_backend.py @@ -14,8 +14,8 @@ class TestBackend(unittest.TestCase): _TEST_CONFIG = { "zerotier": [ { - "id": "9536600adf0af076", - "nwid": "9536600adf0af076", + "id": "9536600adf654321", + "nwid": "9536600adf654321", "objtype": "network", "revision": 1, "creationTime": 1632012345, @@ -57,7 +57,29 @@ class TestBackend(unittest.TestCase): "tags": [{"default": 1, "id": 1}], "remoteTraceTarget": "7f5d90eb87", "remoteTraceLevel": 1, - } + }, + { + "id": "9536600adf654322", + "nwid": "9536600adf654322", + "objtype": "network", + "revision": 1, + "creationTime": 1632012345, + "name": "zerotier-openwisp-network-2", + "private": True, + "enableBroadcast": True, + "v4AssignMode": {"zt": True}, + "v6AssignMode": {"6plane": False, "rfc4193": True, "zt": True}, + "mtu": 2700, + "multicastLimit": 16, + "routes": [{"target": "10.0.0.0/24", "via": "10.0.0.1"}], + "ipAssignmentPools": [ + {"ipRangeStart": "10.0.0.10", "ipRangeEnd": "10.0.0.100"} + ], + "dns": {"domain": "zerotier.openwisp.io", "servers": ["10.147.20.3"]}, + "tags": [{"default": 1, "id": 1}], + "remoteTraceTarget": "7f5d90eb87", + "remoteTraceLevel": 1, + }, ] } @@ -70,63 +92,208 @@ def test_test_schema(self): def test_confs(self): c = ZeroTier(self._TEST_CONFIG) - expected = """# zerotier config: 9536600adf0af076 + expected = """// zerotier config: 9536600adf654321 + +{ + "capabilities": [ +{ + "default": true, + "id": 1, + "rules": [ +{ + "etherType": 2048, + "not": true, + "or": false, + "type": "MATCH_ETHERTYPE" +} + ] +} + ], + "creationTime": 1632012345, + "dns": { +"domain": "zerotier.openwisp.io", +"servers": [ + "10.147.20.3" +] + }, + "enableBroadcast": true, + "id": "9536600adf654321", + "ipAssignmentPools": [ +{ + "ipRangeEnd": "10.0.0.100", + "ipRangeStart": "10.0.0.10" +} + ], + "mtu": 2700, + "multicastLimit": 16, + "name": "zerotier-openwisp-network", + "nwid": "9536600adf654321", + "objtype": "network", + "private": true, + "remoteTraceLevel": 1, + "remoteTraceTarget": "7f5d90eb87", + "revision": 1, + "routes": [ +{ + "target": "10.0.0.0/24", + "via": "10.0.0.1" +} + ], + "rules": [ +{ + "etherType": 2048, + "not": true, + "or": false, + "type": "MATCH_ETHERTYPE" +}, +{ + "type": "ACTION_DROP" +} + ], + "tags": [ +{ + "default": 1, + "id": 1 +} + ], + "v4AssignMode": { +"zt": true + }, + "v6AssignMode": { +"6plane": false, +"rfc4193": true, +"zt": true + } +} + +// zerotier config: 9536600adf654322 -CAP={'default': True, 'id': 1, 'rules': [{'etherType': 2048, 'not': True, 'or': False, 'type': 'MATCH_ETHERTYPE'}]} -DNS=domain -DNS=servers -I={'ipRangeStart': '10.0.0.10', 'ipRangeEnd': '10.0.0.100'} -R={'etherType': 2048, 'not': True, 'or': False, 'type': 'MATCH_ETHERTYPE'} -R={'type': 'ACTION_DROP'} -RT={'target': '10.0.0.0/24', 'via': '10.0.0.1'} -TAG={'default': 1, 'id': 1} -eb=True -ml=16 -mtu=2700 -n=zerotier-openwisp-network -nwid=9536600adf0af076 -p=True -r=1 -t=network -tl=1 -ts=1632012345 -tt=7f5d90eb87 -v4s=zt -v6s=6plane -v6s=rfc4193 -v6s=zt -""" # noqa +{ + "creationTime": 1632012345, + "dns": { +"domain": "zerotier.openwisp.io", +"servers": [ + "10.147.20.3" +] + }, + "enableBroadcast": true, + "id": "9536600adf654322", + "ipAssignmentPools": [ +{ + "ipRangeEnd": "10.0.0.100", + "ipRangeStart": "10.0.0.10" +} + ], + "mtu": 2700, + "multicastLimit": 16, + "name": "zerotier-openwisp-network-2", + "nwid": "9536600adf654322", + "objtype": "network", + "private": true, + "remoteTraceLevel": 1, + "remoteTraceTarget": "7f5d90eb87", + "revision": 1, + "routes": [ +{ + "target": "10.0.0.0/24", + "via": "10.0.0.1" +} + ], + "tags": [ +{ + "default": 1, + "id": 1 +} + ], + "v4AssignMode": { +"zt": true + }, + "v6AssignMode": { +"6plane": false, +"rfc4193": true, +"zt": true + } +} +""" self.assertEqual(c.render(), expected) def test_generate(self): c = ZeroTier(self._TEST_CONFIG) - expected = """CAP={'default': True, 'id': 1, 'rules': [{'etherType': 2048, 'not': True, 'or': False, 'type': 'MATCH_ETHERTYPE'}]} -DNS=domain -DNS=servers -I={'ipRangeStart': '10.0.0.10', 'ipRangeEnd': '10.0.0.100'} -R={'etherType': 2048, 'not': True, 'or': False, 'type': 'MATCH_ETHERTYPE'} -R={'type': 'ACTION_DROP'} -RT={'target': '10.0.0.0/24', 'via': '10.0.0.1'} -TAG={'default': 1, 'id': 1} -eb=True -ml=16 -mtu=2700 -n=zerotier-openwisp-network -nwid=9536600adf0af076 -p=True -r=1 -t=network -tl=1 -ts=1632012345 -tt=7f5d90eb87 -v4s=zt -v6s=6plane -v6s=rfc4193 -v6s=zt -""" # noqa + expected = """{ + "capabilities": [ +{ + "default": true, + "id": 1, + "rules": [ +{ + "etherType": 2048, + "not": true, + "or": false, + "type": "MATCH_ETHERTYPE" +} + ] +} + ], + "creationTime": 1632012345, + "dns": { +"domain": "zerotier.openwisp.io", +"servers": [ + "10.147.20.3" +] + }, + "enableBroadcast": true, + "id": "9536600adf654321", + "ipAssignmentPools": [ +{ + "ipRangeEnd": "10.0.0.100", + "ipRangeStart": "10.0.0.10" +} + ], + "mtu": 2700, + "multicastLimit": 16, + "name": "zerotier-openwisp-network", + "nwid": "9536600adf654321", + "objtype": "network", + "private": true, + "remoteTraceLevel": 1, + "remoteTraceTarget": "7f5d90eb87", + "revision": 1, + "routes": [ +{ + "target": "10.0.0.0/24", + "via": "10.0.0.1" +} + ], + "rules": [ +{ + "etherType": 2048, + "not": true, + "or": false, + "type": "MATCH_ETHERTYPE" +}, +{ + "type": "ACTION_DROP" +} + ], + "tags": [ +{ + "default": 1, + "id": 1 +} + ], + "v4AssignMode": { +"zt": true + }, + "v6AssignMode": { +"6plane": false, +"rfc4193": true, +"zt": true + } +} +""" tar = tarfile.open(fileobj=c.generate(), mode="r") - self.assertEqual(len(tar.getmembers()), 1) - vpn = tar.getmember("9536600adf0af076.conf") + self.assertEqual(len(tar.getmembers()), 2) + vpn = tar.getmember("9536600adf654321.json") contents = tar.extractfile(vpn).read().decode() self.assertEqual(contents, expected) @@ -134,7 +301,7 @@ def test_auto_client(self): expected = { "zerotier": [ { - "id": "9536600adf0af076", + "id": "9536600adf654321", "name": "zerotier-openwisp-network", } ]