diff --git a/docs/source/backends/openwrt.rst b/docs/source/backends/openwrt.rst index 4f40cc26e..bcc689740 100644 --- a/docs/source/backends/openwrt.rst +++ b/docs/source/backends/openwrt.rst @@ -2446,6 +2446,141 @@ Will be rendered as follows:: option sysfs 'tp-link:blue:wlan2g' option trigger 'phy0tpt' +DDNS settings +------------- + +The ddns settings reside in the ``ddns`` key of the *configuration dictionary*, +which is a custom NetJSON extension not present in the original NetJSON RFC. + +The ``ddns`` key must contain a dictionary, the allowed keys are: + ++---------------------+---------+ +| key name | type | ++=====================+=========+ +| ``upd_privateip`` | boolean | ++---------------------+---------+ +| ``ddns_dateformat`` | string | ++---------------------+---------+ +| ``ddns_rundir`` | string | ++---------------------+---------+ +| ``ddns_logdir`` | string | ++---------------------+---------+ +| ``ddns_loglines`` | integer | ++---------------------+---------+ +| ``use_curl`` | boolean | ++---------------------+---------+ +| ``providers`` | list | ++---------------------+---------+ + +The ``providers`` key itself contains a list of dictionaries, the allowed keys are: + ++---------------------+---------+ +| key name | type | ++=====================+=========+ +| ``enabled`` | boolean | ++---------------------+---------+ +| ``interface`` | string | ++---------------------+---------+ +| ``ip_source`` | string | ++---------------------+---------+ +| ``lookup_host`` | string | ++---------------------+---------+ +| ``domain`` | string | ++---------------------+---------+ +| ``username`` | string | ++---------------------+---------+ +| ``password`` | string | ++---------------------+---------+ +| ``service_name`` | string | ++---------------------+---------+ +| ``update_url`` | string | ++---------------------+---------+ +| ``update_script`` | string | ++---------------------+---------+ +| ``ip_network`` | string | ++---------------------+---------+ +| ``ip_url`` | string | ++---------------------+---------+ +| ``ip_interface`` | string | ++---------------------+---------+ +| ``ip_script`` | string | ++---------------------+---------+ +| ``use_syslog`` | integer | ++---------------------+---------+ +| ``use_logfile`` | boolean | ++---------------------+---------+ + +The required keys are: + +* ``enabled`` +* ``interface`` +* ``ip_source`` +* ``lookup_host`` +* ``domain`` +* ``username`` +* ``password`` + +For the function and meaning of each key consult the relevant +`OpenWrt documentation about ddns directives `_. + +DDNS settings example +~~~~~~~~~~~~~~~~~~~~~ + +The following *configuration dictionary*: + +.. code-block:: python + + { + "ddns": { + "ddns_logdir": "/var/log/ddns", + "ddns_rundir": "/var/run/ddns", + "use_curl": False, + "upd_privateip": False, + "ddns_dateformat": "%F %R", + "ddns_loglines": 250, + "providers": [ + { + "enabled": True, + "lookup_host": "myhost.dyndns.org", + "service_name": "dyndns.org", + "domain": "myhost.dyndns.org", + "username": "myuser", + "password": "mypassword", + "use_logfile": True, + "ip_source": "interface", + "ip_interface": "pppoe-xdsl", + "use_syslog": 2, + "interface": "xdsl" + } + ], + } + } + +Will be rendered as follows:: + + package ddns + + config ddns 'global' + option ddns_logdir '/var/log/ddns' + option ddns_rundir '/var/run/ddns' + option use_curl '0' + option upd_privateip '0' + option ddns_dateformat '%F %R' + option ddns_loglines '250' + + config service 'myhost_dyndns_org' + option enabled '1' + option lookup_host 'myhost.dyndns.org' + option service_name 'dyndns.org' + option domain 'myhost.dyndns.org' + option username 'myuser' + option password 'mypassword' + option use_logfile '1' + option ip_source 'interface' + option ip_interface 'pppoe-xdsl' + option use_syslog '2' + option interface 'xdsl' + Including custom options ------------------------ diff --git a/netjsonconfig/backends/openwrt/converters/__init__.py b/netjsonconfig/backends/openwrt/converters/__init__.py index 5993c2b83..3320f73ef 100644 --- a/netjsonconfig/backends/openwrt/converters/__init__.py +++ b/netjsonconfig/backends/openwrt/converters/__init__.py @@ -1,3 +1,4 @@ +from .ddns import Ddns from .default import Default from .general import General from .interfaces import Interfaces @@ -24,4 +25,5 @@ 'Switch', 'WireguardPeers', 'Wireless', + 'Ddns', ] diff --git a/netjsonconfig/backends/openwrt/converters/ddns.py b/netjsonconfig/backends/openwrt/converters/ddns.py new file mode 100644 index 000000000..27a7a281b --- /dev/null +++ b/netjsonconfig/backends/openwrt/converters/ddns.py @@ -0,0 +1,56 @@ +from collections import OrderedDict + +from ..schema import schema +from .base import OpenWrtConverter + + +class Ddns(OpenWrtConverter): + netjson_key = 'ddns' + intermediate_key = 'ddns' + _uci_types = ['ddns', 'service'] + _schema = schema['properties']['ddns'] + + def to_intermediate_loop(self, block, result, index=None): + if block: + provider_list = self.__intermediate_providers(block.pop('providers', {})) + block.update({'.type': 'ddns', '.name': block.pop('id', 'global')}) + result.setdefault('ddns', []) + result['ddns'] = [self.sorted_dict(block)] + provider_list + + return result + + def __intermediate_providers(self, providers): + """ + converts NetJSON provider to + UCI intermediate data structure + """ + result = [] + + for provider in providers: + uci_name = self._get_uci_name(provider['lookup_host']) + resultdict = OrderedDict((('.name', uci_name), ('.type', 'service'))) + resultdict.update(provider) + result.append(resultdict) + + return result + + def to_netjson_loop(self, block, result, index): + result.setdefault(self.netjson_key, {}) + + if block['.type'] == 'service': + result[self.netjson_key].setdefault('providers', []) + result[self.netjson_key]['providers'].append(self.__netjson_ddns(block)) + else: + result['ddns'] = self.__netjson_ddns(block) + + return result + + def __netjson_ddns(self, ddns): + _type = ddns.pop('.type') + del ddns['.name'] + + if _type == 'service': + ddns_schema = self._schema.get('properties').get('providers').get('items') + return self.type_cast(ddns, schema=ddns_schema) + + return self.type_cast(ddns) diff --git a/netjsonconfig/backends/openwrt/openwrt.py b/netjsonconfig/backends/openwrt/openwrt.py index 1f156e00b..fbabb34f7 100644 --- a/netjsonconfig/backends/openwrt/openwrt.py +++ b/netjsonconfig/backends/openwrt/openwrt.py @@ -25,6 +25,7 @@ class OpenWrt(BaseBackend): converters.Wireless, converters.OpenVpn, converters.WireguardPeers, + converters.Ddns, converters.Default, ] parser = OpenWrtParser diff --git a/netjsonconfig/backends/openwrt/schema.py b/netjsonconfig/backends/openwrt/schema.py index bf2ffd9d4..a5535adf7 100644 --- a/netjsonconfig/backends/openwrt/schema.py +++ b/netjsonconfig/backends/openwrt/schema.py @@ -928,10 +928,212 @@ }, }, }, + "ddns": { + "type": "object", + "title": "DDNS Settings", + "additionalProperties": True, + "propertyOrder": 11, + "properties": { + "upd_privateip": { + "type": "boolean", + "title": "upd_privateip", + "description": "disallow/allow sending of private/special IP's to the DDNS provider; " + "blocked IPv4: 0/8, 10/8, 100.64/10, 127/8, 169.254/16, 172.16/12, " + "192.168/16; blocked IPv6: ::/32, f000::/4", + "default": False, + "format": "checkbox", + "propertyOrder": 1, + }, + "ddns_dateformat": { + "type": "string", + "title": "ddns_dateformat", + "description": "date format to use for displaying dates in logfiles and LuCI", + "default": "%F %R", + "propertyOrder": 2, + }, + "ddns_rundir": { + "type": "string", + "title": "ddns_rundir", + "description": "directory to use for *.pid and *.update files", + "default": "/var/run/ddns", + "propertyOrder": 3, + }, + "ddns_logdir": { + "type": "string", + "title": "ddns_logdir", + "description": "directory to use for *.log files", + "default": "/var/log/ddns", + "propertyOrder": 4, + }, + "ddns_loglines": { + "type": "integer", + "title": "ddns_loglines", + "description": "number of lines stored in *.log files before automatically truncated", + "default": 250, + "propertyOrder": 5, + }, + "use_curl": { + "type": "boolean", + "title": "use_curl", + "description": "if both wget and curl are installed, wget is used for communication " + "by default", + "default": False, + "format": "checkbox", + "propertyOrder": 6, + }, + "providers": { + "type": "array", + "title": "Service Providers", + "uniqueItems": True, + "additionalItems": True, + "propertyOrder": 7, + "items": { + "type": "object", + "title": "DDNS provider", + "additionalProperties": True, + "required": [ + "enabled", + "interface", + "ip_source", + "lookup_host", + "domain", + "username", + "password", + ], + "properties": { + "enabled": { + "type": "boolean", + "title": "enabled", + "default": False, + "format": "checkbox", + "propertyOrder": 1, + }, + "interface": { + "type": "string", + "title": "interface", + "description": "network from /etc/config/network to monitor for up/down " + "events to start the ddns update script via hotplug", + "propertyOrder": 2, + }, + "ip_source": { + "type": "string", + "title": "ip_source", + "description": "specifies the source to detect the local IP: " + "'network' uses 'ip_network', 'web' uses 'ip_url', 'interface' uses " + "'ip_interface', 'script' uses 'ip_script'", + "enum": ["network", "web", "interface", "script"], + "default": "network", + "propertyOrder": 3, + }, + "lookup_host": { + "type": "string", + "title": "lookup_host", + "description": "FQDN of the host registered at the DDNS provider", + "propertyOrder": 4, + }, + "domain": { + "type": "string", + "title": "domain", + "description": "the DNS name to update;" + " this property can also be used for " + "special multihost update configurations supported by" + " some providers", + "propertyOrder": 5, + }, + "username": { + "type": "string", + "title": "username", + "description": "username of the DDNS service account", + "propertyOrder": 6, + }, + "password": { + "type": "string", + "title": "password", + "description": "password of the DDNS service account", + "propertyOrder": 7, + }, + "service_name": { + "type": "string", + "title": "service_name", + "description": "name of the DDNS service to use", + "propertyOrder": 8, + }, + "update_url": { + "type": "string", + "title": "update_url", + "description": "url to the DDNS service to use" + " if 'service_name' is not set", + "propertyOrder": 9, + }, + "update_script": { + "type": "string", + "title": "update_script", + "description": "script to use if 'service_name' is not set", + "propertyOrder": 10, + }, + "ip_network": { + "type": "string", + "title": "ip_network", + "description": "network from /etc/config/network to use" + " for detecting the IP if 'ip_source' is set to 'network'", + "default": "wan", + "propertyOrder": 11, + }, + "ip_url": { + "type": "string", + "title": "ip_url", + "description": "url to use for detecting the IP if 'ip_source' is set to " + "'web'", + "propertyOrder": 12, + }, + "ip_interface": { + "type": "string", + "title": "ip_interface", + "description": "local interface to use" + " for detecting the IP if 'ip_source' is set to 'interface'", + "propertyOrder": 13, + }, + "ip_script": { + "type": "string", + "title": "ip_script", + "description": "script to use for detecting the IP" + " if 'ip_source' is set to 'script'", + "propertyOrder": 14, + }, + "use_syslog": { + "type": "integer", + "title": "use_syslog", + "description": "level of events logged to syslog", + "enum": [0, 1, 2, 3, 4], + "options": { + "enum_titles": [ + "0 - disable", + "1 - info, notice, warning, errors", + "2 - notice, warning, errors", + "3 - warning, errors", + "4 - errors", + ] + }, + "default": 0, + "propertyOrder": 15, + }, + "use_logfile": { + "type": "boolean", + "title": "use_logfile", + "description": "disable/enable logging to logfile", + "default": True, + "propertyOrder": 16, + }, + }, + }, + }, + }, + }, }, }, ) + # add OpenVPN schema schema = merge_config(schema, base_openvpn_schema) # OpenVPN customizations for OpenWRT diff --git a/tests/openwrt/test_ddns.py b/tests/openwrt/test_ddns.py new file mode 100644 index 000000000..09ec05dcb --- /dev/null +++ b/tests/openwrt/test_ddns.py @@ -0,0 +1,66 @@ +import unittest + +from netjsonconfig import OpenWrt +from netjsonconfig.utils import _TabsMixin + + +class TestDdns(unittest.TestCase, _TabsMixin): + maxDiff = None + _ddns_netjson_global = { + "ddns": { + "ddns_dateformat": "%F %R", + "ddns_logdir": "/var/log/ddns", + "ddns_loglines": 250, + "ddns_rundir": "/var/run/ddns", + "upd_privateip": False, + "use_curl": False, + "providers": [ + { + "enabled": True, + "lookup_host": "myhost.dyndns.org", + "service_name": "dyndns.org", + "domain": "myhost.dyndns.org", + "username": "myuser", + "password": "mypassword", + "use_logfile": True, + "ip_source": "interface", + "ip_interface": "pppoe-xdsl", + "use_syslog": 2, + "interface": "xdsl", + } + ], + } + } + _ddns_uci_global = """package ddns + +config ddns 'global' + option ddns_dateformat '%F %R' + option ddns_logdir '/var/log/ddns' + option ddns_loglines '250' + option ddns_rundir '/var/run/ddns' + option upd_privateip '0' + option use_curl '0' + +config service 'myhost_dyndns_org' + option enabled '1' + option lookup_host 'myhost.dyndns.org' + option service_name 'dyndns.org' + option domain 'myhost.dyndns.org' + option username 'myuser' + option password 'mypassword' + option use_logfile '1' + option ip_source 'interface' + option ip_interface 'pppoe-xdsl' + option use_syslog '2' + option interface 'xdsl' +""" + + def test_render_ddns_global(self): + result = OpenWrt(self._ddns_netjson_global).render() + expected = self._tabs(self._ddns_uci_global) + self.assertEqual(result, expected) + + def test_parse_ddns_global(self): + result = OpenWrt(native=self._ddns_uci_global).config + expected = self._ddns_netjson_global + self.assertDictEqual(result, expected)