From f24763c416a3726e1a20c76947c3cd6801a9d0f2 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Tue, 12 Sep 2023 23:31:43 -0500 Subject: ddclient: T5612: Fix VRF support for ddclient service Fix VRF support interface definition and configuration mode for ddclient to actually capture the VRF name and pass it to the template. --- src/conf_mode/dns_dynamic.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 4b1aed742..712889f68 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -19,6 +19,7 @@ import os from sys import exit from vyos.config import Config +from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos import ConfigError @@ -61,6 +62,10 @@ def verify(dyndns): return None for address in dyndns['address']: + # If dyndns address is an interface, ensure it exists + if address != 'web': + verify_interface_exists(address) + # RFC2136 - configuration validation if 'rfc2136' in dyndns['address'][address]: for config in dyndns['address'][address]['rfc2136'].values(): -- cgit v1.2.3 From c545758552ababa069fc090ac50b79a69ad72457 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 21 Sep 2023 21:35:18 -0500 Subject: ddclient: T5612: Enable TTL support for web-service based protocols Enable TTL support for web-service based protocols in addition to RFC2136 based (nsupdate) protocol. Since TTL is not supported by all protocols, and thus cannot have a configuration default, the existing XML snippet `include/dns/time-to-live.xml.i` does not have common `300` anymore and is instead added explicitly whenever necessary. --- data/templates/dns-dynamic/ddclient.conf.j2 | 2 +- interface-definitions/dns-dynamic.xml.in | 1 + interface-definitions/dns-forwarding.xml.in | 30 ++++++++++++++++++++++ .../include/dns/time-to-live.xml.i | 1 - smoketest/scripts/cli/test_service_dns_dynamic.py | 19 +++++++++++--- src/conf_mode/dns_dynamic.py | 6 +++++ 6 files changed, 53 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 421daf1df..efc7f0fe4 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -63,7 +63,7 @@ use=no {# ddclient default ('ip') results in confusing warning messag # Web service dynamic DNS configuration for {{ name }}: [{{ config.protocol }}, {{ host }}] {{ render_config(host, address, service_cfg.web_options, ip_suffixes, protocol=config.protocol, server=config.server, zone=config.zone, - login=config.username, password=config.password) }} + login=config.username, password=config.password, ttl=config.ttl) }} {% endfor %} {% endfor %} diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 94e0645d4..93b1dbc23 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -101,6 +101,7 @@ #include #include #include + #include ddclient protocol used for Dynamic DNS service diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index c00051c47..5ca02acef 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -158,6 +158,9 @@ #include + + 300 + #include @@ -195,6 +198,9 @@ #include + + 300 + #include @@ -227,6 +233,9 @@ #include + + 300 + #include @@ -274,6 +283,9 @@ #include + + 300 + #include @@ -302,6 +314,9 @@ #include + + 300 + #include @@ -334,6 +349,9 @@ #include + + 300 + #include @@ -364,6 +382,9 @@ #include + + 300 + #include @@ -393,6 +414,9 @@ #include + + 300 + #include @@ -477,6 +501,9 @@ #include + + 300 + #include @@ -585,6 +612,9 @@ #include + + 300 + #include diff --git a/interface-definitions/include/dns/time-to-live.xml.i b/interface-definitions/include/dns/time-to-live.xml.i index 5c1a1472d..000eea108 100644 --- a/interface-definitions/include/dns/time-to-live.xml.i +++ b/interface-definitions/include/dns/time-to-live.xml.i @@ -10,6 +10,5 @@ - 300 diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 366b063c7..aa4891829 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -63,18 +63,29 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ddns + [svc, 'host-name', hostname]) self.cli_set(base_path + ddns + [svc, 'password', password]) self.cli_set(base_path + ddns + [svc, 'zone', zone]) + self.cli_set(base_path + ddns + [svc, 'ttl', ttl]) for opt, value in details.items(): self.cli_set(base_path + ddns + [svc, opt, value]) - # commit changes + # 'zone' option is supported and required by 'cloudfare', but not 'freedns' and 'zoneedit' + self.cli_set(base_path + ddns + [svc, 'zone', zone]) + if details['protocol'] == 'cloudflare': + pass + else: + # exception is raised for unsupported ones + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ddns + [svc, 'zone']) + + # 'ttl' option is supported by 'cloudfare', but not 'freedns' and 'zoneedit' + self.cli_set(base_path + ddns + [svc, 'ttl', ttl]) if details['protocol'] == 'cloudflare': pass else: - # zone option does not work on all protocols, an exception is - # raised for all others + # exception is raised for unsupported ones with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(base_path + ddns + [svc, 'zone', zone]) + self.cli_delete(base_path + ddns + [svc, 'ttl']) # commit changes self.cli_commit() diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 712889f68..2885f3e37 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -35,6 +35,9 @@ zone_allowed = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] # Protocols that do not require username username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla'] +# Protocols that support TTL +ttl_supported = ['cloudflare', 'gandi', 'hetzner', 'dnsexit', 'godaddy', 'nfsn'] + # Protocols that support both IPv4 and IPv6 dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla'] @@ -93,6 +96,9 @@ def verify(dyndns): if 'username' not in config: raise ConfigError(f'"username" {error_msg}') + if config['protocol'] not in ttl_supported and 'ttl' in config: + raise ConfigError(f'"{config["protocol"]}" does not support "ttl"') + if config['ip_version'] == 'both': if config['protocol'] not in dualstack_supported: raise ConfigError(f'"{config["protocol"]}" does not support ' -- cgit v1.2.3 From 6023328a637ebb84e9d90145a87b63262bc5af63 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sat, 16 Sep 2023 13:33:02 -0500 Subject: ddclient: T5612: Improve dual stack support for dyndns2 protocol dyndns2 protocol in ddclient honors dual stack for selective servers because of the way it is implemented in ddclient. We formalize the well known servers that support dual stack in a list and check against it when validating the configuration. --- src/conf_mode/dns_dynamic.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 2885f3e37..5150574a8 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,7 +30,7 @@ config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' # Protocols that require zone -zone_allowed = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_required = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] # Protocols that do not require username username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla'] @@ -41,6 +41,10 @@ ttl_supported = ['cloudflare', 'gandi', 'hetzner', 'dnsexit', 'godaddy', 'nfsn'] # Protocols that support both IPv4 and IPv6 dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla'] +# dyndns2 protocol in ddclient honors dual stack for selective servers +# because of the way it is implemented in ddclient +dyndns_dualstack_servers = ['members.dyndns.org', 'dynv6.com'] + def get_config(config=None): if config: conf = config @@ -86,10 +90,10 @@ def verify(dyndns): if field not in config: raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}') - if config['protocol'] in zone_allowed and 'zone' not in config: + if config['protocol'] in zone_required and 'zone' not in config: raise ConfigError(f'"zone" {error_msg}') - if config['protocol'] not in zone_allowed and 'zone' in config: + if config['protocol'] not in zone_required and 'zone' in config: raise ConfigError(f'"{config["protocol"]}" does not support "zone"') if config['protocol'] not in username_unnecessary: @@ -104,7 +108,7 @@ def verify(dyndns): raise ConfigError(f'"{config["protocol"]}" does not support ' f'both IPv4 and IPv6 at the same time') # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org) - if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] != 'members.dyndns.org': + if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers: raise ConfigError(f'"{config["protocol"]}" does not support ' f'both IPv4 and IPv6 at the same time for "{config["server"]}"') -- cgit v1.2.3 From a886b2efdd4dde56b407c1e860485a79b5bad210 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Fri, 22 Sep 2023 12:29:22 -0500 Subject: ddclient: T5612: Adjust validator and completion for ddclient Adjust the validator and completion for ddclient to remove unsupported or superfluous protocols. Specifically, - remove 'nsupdate' protocol from the list because there is a separate config path for that protocol (rfc2136) - remove 'cloudns' protocol from the list because it has non standard configuration and is not supported by our configurator at this time --- src/completion/list_ddclient_protocols.sh | 2 +- src/validators/ddclient-protocol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh index 75fb0cf44..3b4eff4d6 100755 --- a/src/completion/list_ddclient_protocols.sh +++ b/src/completion/list_ddclient_protocols.sh @@ -14,4 +14,4 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -echo -n $(ddclient -list-protocols) +echo -n $(ddclient -list-protocols | grep -vE 'nsupdate|cloudns') diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol index 6f927927b..bc6826120 100755 --- a/src/validators/ddclient-protocol +++ b/src/validators/ddclient-protocol @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -ddclient -list-protocols | grep -qw $1 +ddclient -list-protocols | grep -vE 'nsupdate|cloudns' | grep -qw $1 if [ $? -gt 0 ]; then echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols" -- cgit v1.2.3 From 6b30a92eaff48ae5dd4968e30f3464e04c69d4fd Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 21 Sep 2023 21:11:40 -0500 Subject: ddclient: T5612: Additional refactoring for scripts and smoketests Additional cleanup and refactoring for ddclient scripts including the smotektests. --- smoketest/scripts/cli/test_service_dns_dynamic.py | 115 +++++++++++----------- src/conf_mode/dns_dynamic.py | 21 ++-- 2 files changed, 69 insertions(+), 67 deletions(-) (limited to 'src') diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index f1870320e..66dcde434 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -32,12 +32,19 @@ DDCLIENT_PID = '/run/ddclient/ddclient.pid' DDCLIENT_PNAME = 'ddclient' base_path = ['service', 'dns', 'dynamic'] +server = 'ddns.vyos.io' hostname = 'test.ddns.vyos.io' zone = 'vyos.io' +username = 'vyos_user' password = 'paSS_@4ord' +ttl = '300' interface = 'eth0' class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): + def setUp(self): + # Always start with a clean CLI instance + self.cli_delete(base_path) + def tearDown(self): # Check for running process self.assertTrue(process_running(DDCLIENT_PID)) @@ -51,41 +58,38 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv4 standard DDNS service configuration def test_01_dyndns_service_standard(self): - ddns = ['address', interface, 'service'] + svc_path = ['address', interface, 'service'] services = {'cloudflare': {'protocol': 'cloudflare'}, - 'freedns': {'protocol': 'freedns', 'username': 'vyos_user'}, - 'zoneedit': {'protocol': 'zoneedit1', 'username': 'vyos_user'}} + 'freedns': {'protocol': 'freedns', 'username': username}, + 'zoneedit': {'protocol': 'zoneedit1', 'username': username}} for svc, details in services.items(): - # Always start with a clean CLI instance - self.cli_delete(base_path) - - self.cli_set(base_path + ddns + [svc, 'host-name', hostname]) - self.cli_set(base_path + ddns + [svc, 'password', password]) - self.cli_set(base_path + ddns + [svc, 'zone', zone]) - self.cli_set(base_path + ddns + [svc, 'ttl', ttl]) + self.cli_set(base_path + svc_path + [svc, 'host-name', hostname]) + self.cli_set(base_path + svc_path + [svc, 'password', password]) + self.cli_set(base_path + svc_path + [svc, 'zone', zone]) + self.cli_set(base_path + svc_path + [svc, 'ttl', ttl]) for opt, value in details.items(): - self.cli_set(base_path + ddns + [svc, opt, value]) + self.cli_set(base_path + svc_path + [svc, opt, value]) # 'zone' option is supported and required by 'cloudfare', but not 'freedns' and 'zoneedit' - self.cli_set(base_path + ddns + [svc, 'zone', zone]) + self.cli_set(base_path + svc_path + [svc, 'zone', zone]) if details['protocol'] == 'cloudflare': pass else: # exception is raised for unsupported ones with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(base_path + ddns + [svc, 'zone']) + self.cli_delete(base_path + svc_path + [svc, 'zone']) # 'ttl' option is supported by 'cloudfare', but not 'freedns' and 'zoneedit' - self.cli_set(base_path + ddns + [svc, 'ttl', ttl]) + self.cli_set(base_path + svc_path + [svc, 'ttl', ttl]) if details['protocol'] == 'cloudflare': pass else: # exception is raised for unsupported ones with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(base_path + ddns + [svc, 'ttl']) + self.cli_delete(base_path + svc_path + [svc, 'ttl']) # commit changes self.cli_commit() @@ -109,20 +113,17 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv6 only DDNS service configuration def test_02_dyndns_service_ipv6(self): timeout = '60' - ddns = ['address', interface, 'service', 'dynv6'] + svc_path = ['address', interface, 'service', 'dynv6'] proto = 'dyndns2' - user = 'none' - password = 'paSS_4ord' - srv = 'ddns.vyos.io' ip_version = 'ipv6' self.cli_set(base_path + ['timeout', timeout]) - self.cli_set(base_path + ddns + ['ip-version', ip_version]) - self.cli_set(base_path + ddns + ['protocol', proto]) - self.cli_set(base_path + ddns + ['server', srv]) - self.cli_set(base_path + ddns + ['username', user]) - self.cli_set(base_path + ddns + ['password', password]) - self.cli_set(base_path + ddns + ['host-name', hostname]) + self.cli_set(base_path + svc_path + ['ip-version', ip_version]) + self.cli_set(base_path + svc_path + ['protocol', proto]) + self.cli_set(base_path + svc_path + ['server', server]) + self.cli_set(base_path + svc_path + ['username', username]) + self.cli_set(base_path + svc_path + ['password', password]) + self.cli_set(base_path + svc_path + ['host-name', hostname]) # commit changes self.cli_commit() @@ -133,37 +134,45 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'usev6=ifv6', ddclient_conf) self.assertIn(f'ifv6={interface}', ddclient_conf) self.assertIn(f'protocol={proto}', ddclient_conf) - self.assertIn(f'server={srv}', ddclient_conf) - self.assertIn(f'login={user}', ddclient_conf) + self.assertIn(f'server={server}', ddclient_conf) + self.assertIn(f'login={username}', ddclient_conf) self.assertIn(f'password={password}', ddclient_conf) # IPv4+IPv6 dual DDNS service configuration def test_03_dyndns_service_dual_stack(self): - ddns = ['address', interface, 'service'] - services = {'cloudflare': {'protocol': 'cloudflare', 'zone': 'vyos.io'}, - 'freedns': {'protocol': 'freedns', 'username': 'vyos_user'}} - password = 'vyos_pass' + svc_path = ['address', interface, 'service'] + services = {'cloudflare': {'protocol': 'cloudflare', 'zone': zone}, + 'freedns': {'protocol': 'freedns', 'username': username}, + 'google': {'protocol': 'googledomains', 'username': username}} ip_version = 'both' - for svc, details in services.items(): - # Always start with a clean CLI instance - self.cli_delete(base_path) - - self.cli_set(base_path + ddns + [svc, 'host-name', hostname]) - self.cli_set(base_path + ddns + [svc, 'password', password]) - self.cli_set(base_path + ddns + [svc, 'ip-version', ip_version]) + for name, details in services.items(): + self.cli_set(base_path + svc_path + [name, 'host-name', hostname]) + self.cli_set(base_path + svc_path + [name, 'password', password]) for opt, value in details.items(): - self.cli_set(base_path + ddns + [svc, opt, value]) + self.cli_set(base_path + svc_path + [name, opt, value]) + + # Dual stack is supported by 'cloudfare' and 'freedns' but not 'googledomains' + # exception is raised for unsupported ones + self.cli_set(base_path + svc_path + [name, 'ip-version', ip_version]) + if details['protocol'] not in ['cloudflare', 'freedns']: + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + svc_path + [name, 'ip-version']) # commit changes self.cli_commit() # Check the generating config parameters ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') - self.assertIn(f'usev4=ifv4', ddclient_conf) - self.assertIn(f'usev6=ifv6', ddclient_conf) - self.assertIn(f'ifv4={interface}', ddclient_conf) - self.assertIn(f'ifv6={interface}', ddclient_conf) + if details['protocol'] not in ['cloudflare', 'freedns']: + self.assertIn(f'usev4=ifv4', ddclient_conf) + self.assertIn(f'ifv4={interface}', ddclient_conf) + else: + self.assertIn(f'usev4=ifv4', ddclient_conf) + self.assertIn(f'usev6=ifv6', ddclient_conf) + self.assertIn(f'ifv4={interface}', ddclient_conf) + self.assertIn(f'ifv6={interface}', ddclient_conf) self.assertIn(f'password={password}', ddclient_conf) for opt in details.keys(): @@ -176,19 +185,16 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def test_04_dyndns_rfc2136(self): # Check if DDNS service can be configured and runs - ddns = ['address', interface, 'rfc2136', 'vyos'] - srv = 'ns1.vyos.io' - zone = 'vyos.io' - ttl = '300' + svc_path = ['address', interface, 'rfc2136', 'vyos'] with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file: key_file.write(b'S3cretKey') - self.cli_set(base_path + ddns + ['server', srv]) - self.cli_set(base_path + ddns + ['zone', zone]) - self.cli_set(base_path + ddns + ['key', key_file.name]) - self.cli_set(base_path + ddns + ['ttl', ttl]) - self.cli_set(base_path + ddns + ['host-name', hostname]) + self.cli_set(base_path + svc_path + ['server', server]) + self.cli_set(base_path + svc_path + ['zone', zone]) + self.cli_set(base_path + svc_path + ['key', key_file.name]) + self.cli_set(base_path + svc_path + ['ttl', ttl]) + self.cli_set(base_path + svc_path + ['host-name', hostname]) # commit changes self.cli_commit() @@ -198,7 +204,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'use=if', ddclient_conf) self.assertIn(f'if={interface}', ddclient_conf) self.assertIn(f'protocol=nsupdate', ddclient_conf) - self.assertIn(f'server={srv}', ddclient_conf) + self.assertIn(f'server={server}', ddclient_conf) self.assertIn(f'zone={zone}', ddclient_conf) self.assertIn(f'password={key_file.name}', ddclient_conf) self.assertIn(f'ttl={ttl}', ddclient_conf) @@ -231,9 +237,6 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): vrf_name = f'vyos-test-{"".join(random.choices(string.ascii_letters + string.digits, k=5))}' svc_path = ['address', interface, 'service', 'cloudflare'] - # Always start with a clean CLI instance - self.cli_delete(base_path) - self.cli_set(['vrf', 'name', vrf_name, 'table', '12345']) self.cli_set(base_path + ['vrf', vrf_name]) diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 5150574a8..8a438cf6f 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,7 +30,7 @@ config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' # Protocols that require zone -zone_required = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_necessary = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] # Protocols that do not require username username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla'] @@ -51,11 +51,11 @@ def get_config(config=None): else: conf = Config() - base_level = ['service', 'dns', 'dynamic'] - if not conf.exists(base_level): + base = ['service', 'dns', 'dynamic'] + if not conf.exists(base): return None - dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), + dyndns = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, with_recursive_defaults=True) @@ -90,15 +90,14 @@ def verify(dyndns): if field not in config: raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}') - if config['protocol'] in zone_required and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg}') + if config['protocol'] in zone_necessary and 'zone' not in config: + raise ConfigError(f'"zone" {error_msg}') - if config['protocol'] not in zone_required and 'zone' in config: - raise ConfigError(f'"{config["protocol"]}" does not support "zone"') + if config['protocol'] not in zone_necessary and 'zone' in config: + raise ConfigError(f'"{config["protocol"]}" does not support "zone"') - if config['protocol'] not in username_unnecessary: - if 'username' not in config: - raise ConfigError(f'"username" {error_msg}') + if config['protocol'] not in username_unnecessary and 'username' not in config: + raise ConfigError(f'"username" {error_msg}') if config['protocol'] not in ttl_supported and 'ttl' in config: raise ConfigError(f'"{config["protocol"]}" does not support "ttl"') -- cgit v1.2.3 From c3ba4527824c9f4d2e53e7fbd0bff4b84c3012f4 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 13 Sep 2023 01:02:12 -0500 Subject: ddclient: T5574: Support per-service cache management for services Add support for per-service cache management for ddclient providers via `wait-time` and `expiry-time` options. This allows for finer-grained control over how often a service is updated and how long the hostname will be cached before being marked expired in ddclient's cache. More specifically, `wait-time` controls how often ddclient will attempt to check for a change in the hostname's IP address, and `expiry-time` controls how often ddclient to a forced update of the hostname's IP address. These options intentionally don't have any default values because they are provider-specific. They get treated similar to the other provider- specific options in that they are only used if defined. --- data/templates/dns-dynamic/ddclient.conf.j2 | 11 ++++----- interface-definitions/dns-dynamic.xml.in | 2 ++ .../dns/dynamic-service-wait-expiry-time.xml.i | 28 ++++++++++++++++++++++ smoketest/scripts/cli/test_service_dns_dynamic.py | 12 ++++++++++ src/conf_mode/dns_dynamic.py | 3 +++ 5 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 interface-definitions/include/dns/dynamic-service-wait-expiry-time.xml.i (limited to 'src') diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 5905b19ea..6e77abdb5 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -14,10 +14,8 @@ if{{ ipv }}={{ address }}, \ {% endif %} {% endfor %} {# Other service options #} -{% for k,v in kwargs.items() %} -{% if v is vyos_defined %} -{{ k }}={{ v }}{{ ',' if not loop.last }} \ -{% endif %} +{% for k,v in kwargs.items() if v is vyos_defined %} +{{ k | replace('_', '-') }}={{ v }}{{ ',' if not loop.last }} \ {% endfor %} {# Actual hostname for the service #} {{ host }} @@ -49,7 +47,6 @@ use=no {{ render_config(host, address, service_cfg.web_options, protocol='nsupdate', server=config.server, zone=config.zone, password=config.key, ttl=config.ttl) }} - {% endfor %} {% endfor %} {% endif %} @@ -66,8 +63,8 @@ use=no # Web service dynamic DNS configuration for {{ name }}: [{{ config.protocol }}, {{ host }}] {{ render_config(host, address, service_cfg.web_options, ip_suffixes, protocol=config.protocol, server=config.server, zone=config.zone, - login=config.username, password=config.password, ttl=config.ttl) }} - + login=config.username, password=config.password, ttl=config.ttl, + min_interval=config.wait_time, max_interval=config.expiry_time) }} {% endfor %} {% endfor %} {% endif %} diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index ba7f426c1..723223f1c 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -61,6 +61,7 @@ #include #include + #include File containing the TSIG secret key shared with remote DNS server @@ -88,6 +89,7 @@ #include #include + #include #include #include #include diff --git a/interface-definitions/include/dns/dynamic-service-wait-expiry-time.xml.i b/interface-definitions/include/dns/dynamic-service-wait-expiry-time.xml.i new file mode 100644 index 000000000..866690cbe --- /dev/null +++ b/interface-definitions/include/dns/dynamic-service-wait-expiry-time.xml.i @@ -0,0 +1,28 @@ + + + + Time in seconds to wait between update attempts + + u32:60-86400 + Time in seconds + + + + + Wait time must be between 60 and 86400 seconds + + + + + Time in seconds for the hostname to be marked expired in cache + + u32:300-2160000 + Time in seconds + + + + + Expiry time must be between 300 and 2160000 seconds + + + diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 66dcde434..acabc0070 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -116,6 +116,9 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): svc_path = ['address', interface, 'service', 'dynv6'] proto = 'dyndns2' ip_version = 'ipv6' + wait_time = '600' + expiry_time_good = '3600' + expiry_time_bad = '360' self.cli_set(base_path + ['timeout', timeout]) self.cli_set(base_path + svc_path + ['ip-version', ip_version]) @@ -124,6 +127,13 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + svc_path + ['username', username]) self.cli_set(base_path + svc_path + ['password', password]) self.cli_set(base_path + svc_path + ['host-name', hostname]) + self.cli_set(base_path + svc_path + ['wait-time', wait_time]) + + # expiry-time must be greater than wait-time, exception is raised otherwise + self.cli_set(base_path + svc_path + ['expiry-time', expiry_time_bad]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + svc_path + ['expiry-time', expiry_time_good]) # commit changes self.cli_commit() @@ -137,6 +147,8 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'server={server}', ddclient_conf) self.assertIn(f'login={username}', ddclient_conf) self.assertIn(f'password={password}', ddclient_conf) + self.assertIn(f'min-interval={wait_time}', ddclient_conf) + self.assertIn(f'max-interval={expiry_time_good}', ddclient_conf) # IPv4+IPv6 dual DDNS service configuration def test_03_dyndns_service_dual_stack(self): diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 8a438cf6f..874c4b689 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -111,6 +111,9 @@ def verify(dyndns): raise ConfigError(f'"{config["protocol"]}" does not support ' f'both IPv4 and IPv6 at the same time for "{config["server"]}"') + if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']): + raise ConfigError(f'"expiry-time" must be greater than "wait-time"') + return None def generate(dyndns): -- cgit v1.2.3 From 78a7f0182a3ae504f8a29502cc064f56769df75a Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 2 Nov 2023 20:44:57 -0500 Subject: ddclient: T5708: Migrate `timeout` to `interval` Time interval in seconds to wait between DNS updates would be a bit more intuitive as `interval` than `timeout`. --- data/templates/dns-dynamic/ddclient.conf.j2 | 2 +- interface-definitions/dns-dynamic.xml.in | 6 +-- .../include/version/dns-dynamic-version.xml.i | 2 +- smoketest/scripts/cli/test_service_dns_dynamic.py | 6 +-- src/migration-scripts/dns-dynamic/1-to-2 | 52 ++++++++++++++++++++++ 5 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 src/migration-scripts/dns-dynamic/1-to-2 (limited to 'src') diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 6e77abdb5..879887a1f 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -21,7 +21,7 @@ if{{ ipv }}={{ address }}, \ {{ host }} {% endmacro %} ### Autogenerated by dns_dynamic.py ### -daemon={{ timeout }} +daemon={{ interval }} syslog=yes ssl=yes pid={{ config_file | replace('.conf', '.pid') }} diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 723223f1c..07b1bf1b8 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -134,9 +134,9 @@ - + - Time in seconds to wait between DNS updates + Interval in seconds to wait between Dynamic DNS updates u32:60-3600 Time in seconds @@ -144,7 +144,7 @@ - Timeout must be between 60 and 3600 seconds + Interval must be between 60 and 3600 seconds 300 diff --git a/interface-definitions/include/version/dns-dynamic-version.xml.i b/interface-definitions/include/version/dns-dynamic-version.xml.i index b25fc6e76..7bdb90a35 100644 --- a/interface-definitions/include/version/dns-dynamic-version.xml.i +++ b/interface-definitions/include/version/dns-dynamic-version.xml.i @@ -1,3 +1,3 @@ - + diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index acabc0070..6c2f584c9 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -112,7 +112,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv6 only DDNS service configuration def test_02_dyndns_service_ipv6(self): - timeout = '60' + interval = '60' svc_path = ['address', interface, 'service', 'dynv6'] proto = 'dyndns2' ip_version = 'ipv6' @@ -120,7 +120,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): expiry_time_good = '3600' expiry_time_bad = '360' - self.cli_set(base_path + ['timeout', timeout]) + self.cli_set(base_path + ['interval', interval]) self.cli_set(base_path + svc_path + ['ip-version', ip_version]) self.cli_set(base_path + svc_path + ['protocol', proto]) self.cli_set(base_path + svc_path + ['server', server]) @@ -140,7 +140,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check the generating config parameters ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') - self.assertIn(f'daemon={timeout}', ddclient_conf) + self.assertIn(f'daemon={interval}', ddclient_conf) self.assertIn(f'usev6=ifv6', ddclient_conf) self.assertIn(f'ifv6={interface}', ddclient_conf) self.assertIn(f'protocol={proto}', ddclient_conf) diff --git a/src/migration-scripts/dns-dynamic/1-to-2 b/src/migration-scripts/dns-dynamic/1-to-2 new file mode 100644 index 000000000..b4679769c --- /dev/null +++ b/src/migration-scripts/dns-dynamic/1-to-2 @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# T5708: +# - migrate "service dns dynamic timeout ..." +# to "service dns dynamic interval ..." + +import sys +from vyos.configtree import ConfigTree + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base_path = ['service', 'dns', 'dynamic'] +timeout_path = base_path + ['timeout'] + +if not config.exists(base_path): + # Nothing to do + sys.exit(0) + +# Migrate "service dns dynamic timeout ..." +# to "service dns dynamic interval ..." +if config.exists(timeout_path): + config.rename(timeout_path, 'interval') + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From e058ee4909728541f3cd63110908c86214bf76c0 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sat, 4 Nov 2023 16:01:02 -0500 Subject: ddclient: T5708: Validate proper use of `web-options` `web-options` is only applicable when using HTTP(S) web request to obtain the IP address. Apply guard for that. --- src/conf_mode/dns_dynamic.py | 4 ++++ src/migration-scripts/dns-dynamic/1-to-2 | 7 +++++++ 2 files changed, 11 insertions(+) (limited to 'src') diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 874c4b689..d71dc22fd 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -81,6 +81,10 @@ def verify(dyndns): raise ConfigError(f'"{field.replace("_", "-")}" is required for RFC2136 ' f'based Dynamic DNS service on "{address}"') + # Dynamic DNS service provider - configuration validation + if 'web_options' in dyndns['address'][address] and address != 'web': + raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') + # Dynamic DNS service provider - configuration validation if 'service' in dyndns['address'][address]: for service, config in dyndns['address'][address]['service'].items(): diff --git a/src/migration-scripts/dns-dynamic/1-to-2 b/src/migration-scripts/dns-dynamic/1-to-2 index b4679769c..8aaedf210 100644 --- a/src/migration-scripts/dns-dynamic/1-to-2 +++ b/src/migration-scripts/dns-dynamic/1-to-2 @@ -17,6 +17,7 @@ # T5708: # - migrate "service dns dynamic timeout ..." # to "service dns dynamic interval ..." +# - remove "service dns dynamic address web-options ..." when != "web" import sys from vyos.configtree import ConfigTree @@ -34,6 +35,7 @@ config = ConfigTree(config_file) base_path = ['service', 'dns', 'dynamic'] timeout_path = base_path + ['timeout'] +address_path = base_path + ['address'] if not config.exists(base_path): # Nothing to do @@ -44,6 +46,11 @@ if not config.exists(base_path): if config.exists(timeout_path): config.rename(timeout_path, 'interval') +# Remove "service dns dynamic address web-options ..." when != "web" +for address in config.list_nodes(address_path): + if config.exists(address_path + [address, 'web-options']) and address != 'web': + config.delete(address_path + [address, 'web-options']) + try: with open(file_name, 'w') as f: f.write(config.to_string()) -- cgit v1.2.3 From 4419244972ad1183ae42665dd453abb19e162ed5 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 2 Nov 2023 20:15:49 -0500 Subject: ddclient: T5708: Migration to 3.11.1 and related improvements - Migrate to ddclient 3.11.1 and enforce debian/control dependency - Add dual stack support for additional protocols - Restrict usage of `porkbun` protocol, VyOS configuration structure isn't compatible with porkbun yet - Improve and cleanup error messages --- debian/control | 2 +- smoketest/scripts/cli/test_service_dns_dynamic.py | 5 ++-- src/completion/list_ddclient_protocols.sh | 2 +- src/conf_mode/dns_dynamic.py | 34 +++++++++++++---------- src/migration-scripts/dns-dynamic/1-to-2 | 11 ++++++++ src/validators/ddclient-protocol | 2 +- 6 files changed, 36 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/debian/control b/debian/control index 42c0b580b..ae54d6ed6 100644 --- a/debian/control +++ b/debian/control @@ -48,7 +48,7 @@ Depends: cron, curl, dbus, - ddclient (>= 3.9.1), + ddclient (>= 3.11.1), dropbear, easy-rsa, etherwake, diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index c6940f01d..69ea5c1b3 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -246,10 +246,11 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'{name}', ddclient_conf) def test_06_dyndns_vrf(self): - vrf_name = f'vyos-test-{"".join(random.choices(string.ascii_letters + string.digits, k=5))}' + vrf_table = "".join(random.choices(string.digits, k=5)) + vrf_name = f'vyos-test-{vrf_table}' svc_path = ['address', interface, 'service', 'cloudflare'] - self.cli_set(['vrf', 'name', vrf_name, 'table', '12345']) + self.cli_set(['vrf', 'name', vrf_name, 'table', vrf_table]) self.cli_set(base_path + ['vrf', vrf_name]) self.cli_set(base_path + svc_path + ['protocol', 'cloudflare']) diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh index 3b4eff4d6..c8855b5d1 100755 --- a/src/completion/list_ddclient_protocols.sh +++ b/src/completion/list_ddclient_protocols.sh @@ -14,4 +14,4 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -echo -n $(ddclient -list-protocols | grep -vE 'nsupdate|cloudns') +echo -n $(ddclient -list-protocols | grep -vE 'nsupdate|cloudns|porkbun') diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index d71dc22fd..d6ef620fe 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,16 +30,21 @@ config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' # Protocols that require zone -zone_necessary = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1'] # Protocols that do not require username -username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla'] +username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2', + 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla', + 'regfishde'] # Protocols that support TTL -ttl_supported = ['cloudflare', 'gandi', 'hetzner', 'dnsexit', 'godaddy', 'nfsn'] +ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn'] # Protocols that support both IPv4 and IPv6 -dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla'] +dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns', + 'dyndns2', 'easydns', 'freedns', 'hetzner', 'infomaniak', + 'njalla'] # dyndns2 protocol in ddclient honors dual stack for selective servers # because of the way it is implemented in ddclient @@ -88,32 +93,31 @@ def verify(dyndns): # Dynamic DNS service provider - configuration validation if 'service' in dyndns['address'][address]: for service, config in dyndns['address'][address]['service'].items(): - error_msg = f'is required for Dynamic DNS service "{service}" on "{address}"' + error_msg_req = f'is required for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' + error_msg_uns = f'is not supported for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' for field in ['host_name', 'password', 'protocol']: if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}') + raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') if config['protocol'] in zone_necessary and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg}') + raise ConfigError(f'"zone" {error_msg_req}') - if config['protocol'] not in zone_necessary and 'zone' in config: - raise ConfigError(f'"{config["protocol"]}" does not support "zone"') + if config['protocol'] not in zone_supported and 'zone' in config: + raise ConfigError(f'"zone" {error_msg_uns}') if config['protocol'] not in username_unnecessary and 'username' not in config: - raise ConfigError(f'"username" {error_msg}') + raise ConfigError(f'"username" {error_msg_req}') if config['protocol'] not in ttl_supported and 'ttl' in config: - raise ConfigError(f'"{config["protocol"]}" does not support "ttl"') + raise ConfigError(f'"ttl" {error_msg_uns}') if config['ip_version'] == 'both': if config['protocol'] not in dualstack_supported: - raise ConfigError(f'"{config["protocol"]}" does not support ' - f'both IPv4 and IPv6 at the same time') + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns}') # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org) if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers: - raise ConfigError(f'"{config["protocol"]}" does not support ' - f'both IPv4 and IPv6 at the same time for "{config["server"]}"') + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}"') if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']): raise ConfigError(f'"expiry-time" must be greater than "wait-time"') diff --git a/src/migration-scripts/dns-dynamic/1-to-2 b/src/migration-scripts/dns-dynamic/1-to-2 index 8aaedf210..8b599b57a 100644 --- a/src/migration-scripts/dns-dynamic/1-to-2 +++ b/src/migration-scripts/dns-dynamic/1-to-2 @@ -18,6 +18,8 @@ # - migrate "service dns dynamic timeout ..." # to "service dns dynamic interval ..." # - remove "service dns dynamic address web-options ..." when != "web" +# - migrate "service dns dynamic address service protocol dnsexit" +# to "service dns dynamic address service protocol dnsexit2" import sys from vyos.configtree import ConfigTree @@ -51,6 +53,15 @@ for address in config.list_nodes(address_path): if config.exists(address_path + [address, 'web-options']) and address != 'web': config.delete(address_path + [address, 'web-options']) +# Migrate "service dns dynamic address service protocol dnsexit" +# to "service dns dynamic address service protocol dnsexit2" +for address in config.list_nodes(address_path): + for svc_cfg in config.list_nodes(address_path + [address, 'service']): + if config.exists(address_path + [address, 'service', svc_cfg, 'protocol']): + protocol = config.return_value(address_path + [address, 'service', svc_cfg, 'protocol']) + if protocol == 'dnsexit': + config.set(address_path + [address, 'service', svc_cfg, 'protocol'], 'dnsexit2') + try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol index bc6826120..8f455e12e 100755 --- a/src/validators/ddclient-protocol +++ b/src/validators/ddclient-protocol @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -ddclient -list-protocols | grep -vE 'nsupdate|cloudns' | grep -qw $1 +ddclient -list-protocols | grep -vE 'nsupdate|cloudns|porkbun' | grep -qw $1 if [ $? -gt 0 ]; then echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols" -- cgit v1.2.3 From 521713e40f9605da81ec3ffa7a71b48486d692e2 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 6 Nov 2023 14:45:53 -0600 Subject: ddclient: T5708: Migration to 3.11.1 and related improvements Fix execution bit for migration script --- src/migration-scripts/dns-dynamic/1-to-2 | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 src/migration-scripts/dns-dynamic/1-to-2 (limited to 'src') diff --git a/src/migration-scripts/dns-dynamic/1-to-2 b/src/migration-scripts/dns-dynamic/1-to-2 old mode 100644 new mode 100755 -- cgit v1.2.3 From 7092d85ea7d949e65655debe531e17a2220889ad Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 9 Nov 2023 16:12:58 -0600 Subject: ddclient: T5708: Additional smoketests for web-options Add additional smoketests for web-options validation. Also, format error messages to optionally include protocol name. --- smoketest/scripts/cli/test_service_dns_dynamic.py | 53 ++++++++++++++++++++++- src/conf_mode/dns_dynamic.py | 6 +-- 2 files changed, 55 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 69ea5c1b3..a3b220f69 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -245,7 +245,58 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'password=\'{password}\'', ddclient_conf) self.assertIn(f'{name}', ddclient_conf) - def test_06_dyndns_vrf(self): + def test_06_dyndns_web_options(self): + # Check if DDNS service can be configured and runs + base_path_iface = base_path + ['address', interface] + base_path_web = base_path + ['address', 'web'] + svc_path_iface = base_path_iface + ['service', 'cloudflare'] + svc_path_web = base_path_web + ['service', 'cloudflare'] + proto = 'cloudflare' + web_url_good = 'https://ifconfig.me/ip' + web_url_bad = 'http:/ifconfig.me/ip' + + self.cli_set(svc_path_iface + ['protocol', proto]) + self.cli_set(svc_path_iface + ['zone', zone]) + self.cli_set(svc_path_iface + ['password', password]) + self.cli_set(svc_path_iface + ['host-name', hostname]) + self.cli_set(base_path_iface + ['web-options', 'url', web_url_good]) + + # web-options is supported only with web service based address lookup + # exception is raised for interface based address lookup + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path_iface + ['web-options']) + + # commit changes + self.cli_commit() + + # web-options is supported with web service based address lookup + # this should work, but clear interface based config first + self.cli_delete(base_path_iface) + self.cli_set(svc_path_web + ['protocol', proto]) + self.cli_set(svc_path_web + ['zone', zone]) + self.cli_set(svc_path_web + ['password', password]) + self.cli_set(svc_path_web + ['host-name', hostname]) + + # web-options must be a valid URL + with self.assertRaises(ConfigSessionError) as cm: + self.cli_set(base_path_web + ['web-options', 'url', web_url_bad]) + self.assertIn(f'"{web_url_bad.removeprefix("http:")}" is not a valid URI', str(cm.exception)) + self.cli_set(base_path_web + ['web-options', 'url', web_url_good]) + + # commit changes + self.cli_commit() + + # Check the generating config parameters + ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') + self.assertIn(f'usev4=webv4', ddclient_conf) + self.assertIn(f'webv4={web_url_good}', ddclient_conf) + self.assertIn(f'protocol={proto}', ddclient_conf) + self.assertIn(f'zone={zone}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) + self.assertIn(f'{hostname}', ddclient_conf) + + def test_07_dyndns_vrf(self): vrf_table = "".join(random.choices(string.digits, k=5)) vrf_name = f'vyos-test-{vrf_table}' svc_path = ['address', interface, 'service', 'cloudflare'] diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index d6ef620fe..2bccaee0f 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -93,7 +93,7 @@ def verify(dyndns): # Dynamic DNS service provider - configuration validation if 'service' in dyndns['address'][address]: for service, config in dyndns['address'][address]['service'].items(): - error_msg_req = f'is required for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' + error_msg_req = f'is required for Dynamic DNS service "{service}" on "{address}"' error_msg_uns = f'is not supported for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' for field in ['host_name', 'password', 'protocol']: @@ -101,13 +101,13 @@ def verify(dyndns): raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') if config['protocol'] in zone_necessary and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg_req}') + raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') if config['protocol'] not in zone_supported and 'zone' in config: raise ConfigError(f'"zone" {error_msg_uns}') if config['protocol'] not in username_unnecessary and 'username' not in config: - raise ConfigError(f'"username" {error_msg_req}') + raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"') if config['protocol'] not in ttl_supported and 'ttl' in config: raise ConfigError(f'"ttl" {error_msg_uns}') -- cgit v1.2.3