From 1bc78d73410a367cdc5b511ef746dd1be148b1b6 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 98694f5717f282e10caeded3e4e07891fd5bea18 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 712889f68..84c983ee3 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'] @@ -38,6 +38,10 @@ username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', # 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 @@ -83,10 +87,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: @@ -98,7 +102,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 9bb46a470ca372ca13acca789c14aaa114a19e0f 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 421daf1df..f2a20d4b8 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 35e88be03f013e0ae240b6ec9b73fdd8d36ed75e 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 94e0645d4..8d08bf37d 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 @@ -112,15 +101,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 8088cb8b6aacf9b7003845e4c9081b7f569b6fac 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 f2a20d4b8..5905b19ea 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -66,7 +66,7 @@ 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) }} + 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 8d08bf37d..ba7f426c1 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -90,6 +90,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 86dc47a47..c4295317a 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 84c983ee3..5150574a8 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -35,6 +35,9 @@ zone_required = ['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'] @@ -97,6 +100,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 d9f8ff7e277105e34afaa9164517b03ff7675d76 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 f0a8d0d8c622e67ef66c370747f13d4fdb6fd773 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 6c2a2aa4e93d40c9575a83791daf2d141f387976 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