From ac62ced0a1d1737fd8680f45361298a1720c5903 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Tue, 12 Sep 2023 20:09:16 -0500 Subject: ddclient: T5573: Update config generation aligning with caching fixes Now that the caching fixes are in place, we can update the config to remove legacy treatment of ipv4 related properties. --- data/templates/dns-dynamic/ddclient.conf.j2 | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 3446a9d1b..421daf1df 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -59,11 +59,8 @@ use=no {# ddclient default ('ip') results in confusing warning messag {% endif %} {% for host in config.host_name if config.host_name is vyos_defined %} {% set ip_suffixes = ['v4', 'v6'] if config.ip_version == 'both' - else (['v6'] if config.ip_version == 'ipv6' else ['']) %} + else [config.ip_version[2:]] %} {# 'ipvX' -> 'vX' #} # Web service dynamic DNS configuration for {{ name }}: [{{ config.protocol }}, {{ host }}] -{# For ipv4 only setup or legacy ipv6 setup, don't append 'new-style' compliant suffix - ('usev4', 'ifv4', 'webv4' etc.) to the properties and instead live through the - deprecation warnings for better compatibility with most ddclient protocols. #} {{ 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) }} -- cgit v1.2.3 From bda5d89e7483cae1ce74022b1065b53c669dd758 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 18 Sep 2023 04:23:00 -0500 Subject: ddclient: T5573: Fix smoketest for updated ddclient config --- smoketest/scripts/cli/test_service_dns_dynamic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index ee8a07b37..357c3dfb1 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -79,8 +79,8 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') # default value 300 seconds self.assertIn(f'daemon=300', ddclient_conf) - self.assertIn(f'use=if', ddclient_conf) - self.assertIn(f'if={interface}', ddclient_conf) + self.assertIn(f'usev4=ifv4', ddclient_conf) + self.assertIn(f'ifv4={interface}', ddclient_conf) self.assertIn(f'password={password}', ddclient_conf) for opt in details.keys(): -- cgit v1.2.3 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. --- data/templates/dns-dynamic/override.conf.j2 | 2 +- interface-definitions/dns-dynamic.xml.in | 1 + smoketest/scripts/cli/test_service_dns_dynamic.py | 34 +++++++++++++++++++++++ src/conf_mode/dns_dynamic.py | 5 ++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/data/templates/dns-dynamic/override.conf.j2 b/data/templates/dns-dynamic/override.conf.j2 index 6ca1b8a45..4a6851cef 100644 --- a/data/templates/dns-dynamic/override.conf.j2 +++ b/data/templates/dns-dynamic/override.conf.j2 @@ -7,4 +7,4 @@ After=vyos-router.service PIDFile={{ config_file | replace('.conf', '.pid') }} EnvironmentFile= ExecStart= -ExecStart=/usr/bin/ddclient -file {{ config_file }} +ExecStart={{ vrf_command }}/usr/bin/ddclient -file {{ config_file }} diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index a0720f3aa..94e0645d4 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -164,6 +164,7 @@ 300 + #include diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 357c3dfb1..366b063c7 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -17,6 +17,8 @@ import os import unittest import tempfile +import random +import string from base_vyostest_shim import VyOSUnitTestSHIM @@ -24,8 +26,10 @@ from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd from vyos.utils.process import process_running +DDCLIENT_SYSTEMD_UNIT = '/run/systemd/system/ddclient.service.d/override.conf' DDCLIENT_CONF = '/run/ddclient/ddclient.conf' DDCLIENT_PID = '/run/ddclient/ddclient.pid' +DDCLIENT_PNAME = 'ddclient' base_path = ['service', 'dns', 'dynamic'] hostname = 'test.ddns.vyos.io' @@ -188,5 +192,35 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'password={key_file.name}', ddclient_conf) self.assertIn(f'ttl={ttl}', ddclient_conf) + def test_05_dyndns_vrf(self): + 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]) + + self.cli_set(base_path + svc_path + ['protocol', 'cloudflare']) + self.cli_set(base_path + svc_path + ['host-name', hostname]) + self.cli_set(base_path + svc_path + ['zone', zone]) + self.cli_set(base_path + svc_path + ['password', password]) + + # commit changes + self.cli_commit() + + # Check for process in VRF + systemd_override = cmd(f'cat {DDCLIENT_SYSTEMD_UNIT}') + self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient -file {DDCLIENT_CONF}', + systemd_override) + + # Check for process in VRF + proc = cmd(f'ip vrf pids {vrf_name}') + self.assertIn(DDCLIENT_PNAME, proc) + + # Cleanup VRF + self.cli_delete(['vrf', 'name', vrf_name]) + if __name__ == '__main__': unittest.main(verbosity=2) 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(-) 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 25588799dd5f655ee093b650f607d0bab1fd3d00 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sat, 23 Sep 2023 19:06:03 -0500 Subject: ddclient: T5612: Relax hostname validation for apex and wildcard entry Some porvides (like 'namecheap') allow to use '@' or '*' as hostname prefix for apex and wildcard records. This commit relaxes the hostname validation to allow these prefixes. --- .../dns/dynamic-service-host-name-server.xml.i | 3 ++- smoketest/scripts/cli/test_service_dns_dynamic.py | 26 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i b/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i index ee1af2a36..9dd14f97c 100644 --- a/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i +++ b/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i @@ -4,8 +4,9 @@ Hostname to register with Dynamic DNS service #include + (\@|\*)[-.A-Za-z0-9]* - Host-name must be alphanumeric and can contain hyphens + Host-name must be alphanumeric, can contain hyphens and can be prefixed with '@' or '*' diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index aa4891829..f1870320e 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -203,7 +203,31 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'password={key_file.name}', ddclient_conf) self.assertIn(f'ttl={ttl}', ddclient_conf) - def test_05_dyndns_vrf(self): + def test_05_dyndns_hostname(self): + # Check if DDNS service can be configured and runs + svc_path = ['address', interface, 'service', 'namecheap'] + proto = 'namecheap' + hostnames = ['@', 'www', hostname, f'@.{hostname}'] + + for name in hostnames: + 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', name]) + + # commit changes + self.cli_commit() + + # Check the generating config parameters + ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') + self.assertIn(f'protocol={proto}', ddclient_conf) + self.assertIn(f'server={server}', ddclient_conf) + self.assertIn(f'login={username}', ddclient_conf) + self.assertIn(f'password={password}', ddclient_conf) + 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))}' svc_path = ['address', interface, 'service', 'cloudflare'] -- cgit v1.2.3 From 43873d083660373b7fa1203e3e3faae511ac017a Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 21 Sep 2023 16:12:00 -0500 Subject: ddclient: T5612: Generate more reliable ddclient config Adjust the jinja template to avoid generating incorrect ddclient.conf in some cases. The template is reformatted to guarantee whitespacing and empty line separation. --- data/templates/dns-dynamic/ddclient.conf.j2 | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index efc7f0fe4..5905b19ea 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -28,19 +28,21 @@ syslog=yes ssl=yes pid={{ config_file | replace('.conf', '.pid') }} cache={{ config_file | replace('.conf', '.cache') }} -{# Explicitly override global options for reliability #} -web=googledomains {# ddclient default ('dyndns') doesn't support ssl and results in process lockup #} -use=no {# ddclient default ('ip') results in confusing warning message in log #} +{# ddclient default (web=dyndns) doesn't support ssl and results in process lockup #} +web=googledomains +{# ddclient default (use=ip) results in confusing warning message in log #} +use=no {% if address is vyos_defined %} {% for address, service_cfg in address.items() %} {% if service_cfg.rfc2136 is vyos_defined %} {% for name, config in service_cfg.rfc2136.items() %} {% if config.description is vyos_defined %} -# {{ config.description }} +# {{ config.description }} {% endif %} {% for host in config.host_name if config.host_name is vyos_defined %} + # RFC2136 dynamic DNS configuration for {{ name }}: [{{ config.zone }}, {{ host }}] {# Don't append 'new-style' compliant suffix ('usev4', 'usev6', 'ifv4', 'ifv6' etc.) to the properties since 'nsupdate' doesn't support that yet. #} @@ -54,12 +56,13 @@ use=no {# ddclient default ('ip') results in confusing warning messag {% if service_cfg.service is vyos_defined %} {% for name, config in service_cfg.service.items() %} {% if config.description is vyos_defined %} -# {{ config.description }} +# {{ config.description }} {% endif %} {% for host in config.host_name if config.host_name is vyos_defined %} {% set ip_suffixes = ['v4', 'v6'] if config.ip_version == 'both' - else [config.ip_version[2:]] %} {# 'ipvX' -> 'vX' #} + else [config.ip_version[2:]] %} + # 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, -- 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(-) 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 7a1711419eeaaf6bee837e824eec977b16767427 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 21 Sep 2023 22:04:15 -0500 Subject: ddclient: T5612: Refactor zone configuration Refactor zone configuration to use shared XML snippet for all cases. --- interface-definitions/dns-dynamic.xml.in | 23 ++-------------------- .../include/dns/dynamic-service-zone.xml.i | 14 +++++++++++++ 2 files changed, 16 insertions(+), 21 deletions(-) create mode 100644 interface-definitions/include/dns/dynamic-service-zone.xml.i diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 93b1dbc23..ba7f426c1 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -74,18 +74,7 @@ #include - - - Forwarding zone to be updated - - txt - RFC2136 Zone to be updated - - - - - - + #include @@ -113,15 +102,7 @@ - - - DNS zone to update (not used by all protocols) - - txt - Name of DNS zone - - - + #include IP address version to use diff --git a/interface-definitions/include/dns/dynamic-service-zone.xml.i b/interface-definitions/include/dns/dynamic-service-zone.xml.i new file mode 100644 index 000000000..0cc00468f --- /dev/null +++ b/interface-definitions/include/dns/dynamic-service-zone.xml.i @@ -0,0 +1,14 @@ + + + + DNS zone to be updated + + txt + Name of DNS zone + + + + + + + -- 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(-) 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(-) 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 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 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 535b4c1de059627072ee831437371c9cefd26eba Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 8 Nov 2023 14:11:04 -0600 Subject: ddclient: T5708: Ensure password is always wrapped in quotes Migration to 3.11.1 follow-up: This should make `ddclient.conf` parsing more resilient to edge cases (particularly when `password` isn't the last option right before the host parameter). ddclient config parser applies special treatment to the password field and would unwrap the quotes automatically. Also, switch from now deprecated `use=no` to `use=disabled`. --- data/templates/dns-dynamic/ddclient.conf.j2 | 6 +++--- smoketest/scripts/cli/test_service_dns_dynamic.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 879887a1f..356b8d0d0 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -13,9 +13,9 @@ web-skip{{ ipv }}='{{ web_options.skip }}', \ if{{ ipv }}={{ address }}, \ {% endif %} {% endfor %} -{# Other service options #} +{# Other service options with special treatment for password #} {% for k,v in kwargs.items() if v is vyos_defined %} -{{ k | replace('_', '-') }}={{ v }}{{ ',' if not loop.last }} \ +{{ k | replace('_', '-') }}={{ "'%s'" % (v) if k == 'password' else v }}{{ ',' if not loop.last }} \ {% endfor %} {# Actual hostname for the service #} {{ host }} @@ -29,7 +29,7 @@ cache={{ config_file | replace('.conf', '.cache') }} {# ddclient default (web=dyndns) doesn't support ssl and results in process lockup #} web=googledomains {# ddclient default (use=ip) results in confusing warning message in log #} -use=no +use=disabled {% if address is vyos_defined %} {% for address, service_cfg in address.items() %} diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index 6c2f584c9..c6940f01d 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -100,7 +100,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'daemon=300', ddclient_conf) self.assertIn(f'usev4=ifv4', ddclient_conf) self.assertIn(f'ifv4={interface}', ddclient_conf) - self.assertIn(f'password={password}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) for opt in details.keys(): if opt == 'username': @@ -146,7 +146,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'protocol={proto}', ddclient_conf) self.assertIn(f'server={server}', ddclient_conf) self.assertIn(f'login={username}', ddclient_conf) - self.assertIn(f'password={password}', 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) @@ -185,7 +185,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): 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) + self.assertIn(f'password=\'{password}\'', ddclient_conf) for opt in details.keys(): if opt == 'username': @@ -218,7 +218,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'protocol=nsupdate', 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'password=\'{key_file.name}\'', ddclient_conf) self.assertIn(f'ttl={ttl}', ddclient_conf) def test_05_dyndns_hostname(self): @@ -242,7 +242,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'protocol={proto}', ddclient_conf) self.assertIn(f'server={server}', ddclient_conf) self.assertIn(f'login={username}', ddclient_conf) - self.assertIn(f'password={password}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) self.assertIn(f'{name}', ddclient_conf) def test_06_dyndns_vrf(self): -- 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(+) 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(-) 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 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(-) 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 From 2bbf6253038f1751b9b1776a8938c85135ffdca5 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Fri, 10 Nov 2023 09:27:55 -0600 Subject: ddclient: T5708: Fix VRF table generation in smoketest Ensure that the random VRF table name is 4 digits long, not 5 and stays within the the range of 100 - 65535. --- smoketest/scripts/cli/test_service_dns_dynamic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index a3b220f69..fe213a8ae 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -297,7 +297,8 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'{hostname}', ddclient_conf) def test_07_dyndns_vrf(self): - vrf_table = "".join(random.choices(string.digits, k=5)) + # Table number randomized, but should be within range 100-65535 + vrf_table = "".join(random.choices(string.digits, k=4)) vrf_name = f'vyos-test-{vrf_table}' svc_path = ['address', interface, 'service', 'cloudflare'] -- cgit v1.2.3