From 9ee66654171446dcfc5b544fc21ad000142e967d Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 30 Nov 2023 19:29:53 -0600 Subject: ddclient: T5791: Update dynamic dns configuration path Modify the configuration path to be consistent with the usual dialects of VyoS configuration (wireguard, dns, firewall, etc.) This would also shorten the configuration path and have a unified treatment for RFC2136-based updates and other 'web-service' based updates. While at it, add support for per-service web-options. This would allow for probing different external URLs on a per-service basis. --- data/templates/dns-dynamic/ddclient.conf.j2 | 48 ++---- interface-definitions/dns-dynamic.xml.in | 226 +++++++++++++++++----------- src/completion/list_ddclient_protocols.sh | 2 +- src/conf_mode/dns_dynamic.py | 92 +++++------ src/validators/ddclient-protocol | 2 +- 5 files changed, 204 insertions(+), 166 deletions(-) diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 356b8d0d0..30afb9e64 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -29,44 +29,28 @@ 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=disabled +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 %} +{% if name is vyos_defined %} +{% for service, config in name.items() %} +{% if config.description is vyos_defined %} # {{ 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. #} -{{ 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 %} -{% if service_cfg.service is vyos_defined %} -{% for name, config in service_cfg.service.items() %} -{% if config.description is vyos_defined %} - -# {{ 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:]] %} +{% for host in config.host_name if config.host_name is vyos_defined %} +{# ip_suffixes can be either of ['v4'], ['v6'], ['v4', 'v6'] for all protocols except 'nsupdate' + ip_suffixes must be [''] for nsupdate since it doesn't support usevX/wantipvX yet #} +{% set ip_suffixes = ['v4', 'v6'] if config.ip_version == 'both' + else ([config.ip_version[2:]] if config.protocol != 'nsupdate' + else ['']) %} +{% set password = config.key if config.protocol == 'nsupdate' + else config.password %} -# Web service dynamic DNS configuration for {{ name }}: [{{ config.protocol }}, {{ host }}] -{{ render_config(host, address, service_cfg.web_options, ip_suffixes, +# Web service dynamic DNS configuration for {{ service }}: [{{ config.protocol }}, {{ host }}] +{{ render_config(host, config.address, config.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=password, ttl=config.ttl, min_interval=config.wait_time, max_interval=config.expiry_time) }} -{% endfor %} -{% endfor %} -{% endif %} +{% endfor %} {% endfor %} {% endif %} diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 32c5af9b6..f089f0e52 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -12,27 +12,48 @@ Dynamic DNS - + - Obtain IP address to send Dynamic DNS update for + Dynamic DNS configuration txt - Use interface to obtain the IP address + Dynamic DNS service name - - web - Use HTTP(S) web request to obtain the IP address - - - - web - - - #include - web - + #include + + + ddclient protocol used for Dynamic DNS service + + + + + + + + + + + Obtain IP address to send Dynamic DNS update for + + txt + Use interface to obtain the IP address + + + web + Use HTTP(S) web request to obtain the IP address + + + + web + + + #include + web + + + Options when using HTTP(S) web request to obtain the IP address @@ -50,88 +71,117 @@ - + - RFC2136 nsupdate configuration + IP address version to use - txt - RFC2136 nsupdate service name + _ipv4 + Use only IPv4 address + + + _ipv6 + Use only IPv6 address + + both + Use both IPv4 and IPv6 address + + + ipv4 ipv6 both + + + (ipv[46]|both) + + IP Version must be literal 'ipv4', 'ipv6' or 'both' - - #include - #include - #include - - - File containing the TSIG secret key shared with remote DNS server - - filename - File in /config/auth directory - - - - - - - #include - #include - - - + ipv4 + + + + Hostname to register with Dynamic DNS service + + #include + (\@|\*)[-.A-Za-z0-9]* + + Host-name must be alphanumeric, can contain hyphens and can be prefixed with '@' or '*' + + + + - Dynamic DNS configuration + Remote Dynamic DNS server to send updates to + + ipv4 + IPv4 address of the remote server + + + ipv6 + IPv6 address of the remote server + + + hostname + Fully qualified domain name of the remote server + + + + + + Remote server must be IP address or fully qualified domain name + + + + + DNS zone to be updated txt - Dynamic DNS service name + Name of DNS zone + + + - - #include - #include - #include - #include - #include - #include - - - ddclient protocol used for Dynamic DNS service - - - - - - - - - #include - - - IP address version to use - - _ipv4 - Use only IPv4 address - - - _ipv6 - Use only IPv6 address - - - both - Use both IPv4 and IPv6 address - - - ipv4 ipv6 both - - - (ipv[46]|both) - - IP Version must be literal 'ipv4', 'ipv6' or 'both' - - ipv4 - - - + + #include + #include + + + File containing TSIG authentication key for RFC2136 nsupdate on remote DNS server + + filename + File in /config/auth directory + + + + + + + #include + + + 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/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh index c8855b5d1..634981660 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|porkbun') +echo -n $(ddclient -list-protocols | grep -vE 'cloudns|porkbun') diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 2bccaee0f..3ddc8e7fd 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,16 +30,18 @@ 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', 'digitalocean', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', + 'nfsn', 'nsupdate'] zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1'] # Protocols that do not require username username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla', - 'regfishde'] + 'nsupdate', 'regfishde'] # Protocols that support TTL -ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn'] +ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn', + 'nsupdate'] # Protocols that support both IPv4 and IPv6 dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns', @@ -70,63 +72,65 @@ def get_config(config=None): def verify(dyndns): # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: return None - for address in dyndns['address']: - # If dyndns address is an interface, ensure it exists - if address != 'web': - verify_interface_exists(address) + # Dynamic DNS service provider - configuration validation + for service, config in dyndns['name'].items(): - # RFC2136 - configuration validation - if 'rfc2136' in dyndns['address'][address]: - for config in dyndns['address'][address]['rfc2136'].values(): - for field in ['host_name', 'zone', 'server', 'key']: - if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" is required for RFC2136 ' - f'based Dynamic DNS service on "{address}"') + error_msg_req = f'is required for Dynamic DNS service "{service}"' + error_msg_uns = f'is not supported for Dynamic DNS service "{service}"' - # 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') + for field in ['protocol', 'address', 'host_name']: + if field not in config: + raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') - # 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}"' - error_msg_uns = f'is not supported for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' + # If dyndns address is an interface, ensure that it exists + # and that web-options are not set + if config['address'] != 'web': + verify_interface_exists(config['address']) + if 'web_options' in config: + raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') - for field in ['host_name', 'password', 'protocol']: - if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') + # RFC2136 uses 'key' instead of 'password' + if config['protocol'] != 'nsupdate' and 'password' not in config: + raise ConfigError(f'"password" {error_msg_req}') - if config['protocol'] in zone_necessary and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') + # Other RFC2136 specific configuration validation + if config['protocol'] == 'nsupdate': + if 'password' in config: + raise ConfigError(f'"password" {error_msg_uns} with protocol "{config["protocol"]}"') + for field in ['server', 'key']: + if field not in config: + raise ConfigError(f'"{field}" {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'] in zone_necessary and 'zone' not in config: + raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') - if config['protocol'] not in username_unnecessary and 'username' not in config: - raise ConfigError(f'"username" {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} with protocol "{config["protocol"]}"') - if config['protocol'] not in ttl_supported and 'ttl' in config: - raise ConfigError(f'"ttl" {error_msg_uns}') + if config['protocol'] not in username_unnecessary and 'username' not in config: + raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"') - if config['ip_version'] == 'both': - if config['protocol'] not in dualstack_supported: - 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'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}"') + if config['protocol'] not in ttl_supported and 'ttl' in config: + raise ConfigError(f'"ttl" {error_msg_uns} with protocol "{config["protocol"]}"') - 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"') + if config['ip_version'] == 'both': + if config['protocol'] not in dualstack_supported: + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} with protocol "{config["protocol"]}"') + # 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'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}" with protocol "{config["protocol"]}"') + + 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" for Dynamic DNS service "{service}"') return None def generate(dyndns): # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: return None render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600) @@ -139,7 +143,7 @@ def apply(dyndns): call('systemctl daemon-reload') # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol index 8f455e12e..ce5efbd52 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|porkbun' | grep -qw $1 +ddclient -list-protocols | grep -vE '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 040399279c6735a38d3d4a00fe5d97059b7e1945 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 30 Nov 2023 19:31:00 -0600 Subject: ddclient: T5791: Remove XML includes that aren't used anymore As followup to interface definition change, remove XML snippets that aren't used anymore. They were there because they were 'include'-ed multiple times in the interface definition `dynamic-dns.xml.in`. Since that's not the case anymore, they can be removed. --- .../dns/dynamic-service-host-name-server.xml.i | 35 ---------------------- .../dns/dynamic-service-wait-expiry-time.xml.i | 28 ----------------- .../include/dns/dynamic-service-zone.xml.i | 14 --------- 3 files changed, 77 deletions(-) delete mode 100644 interface-definitions/include/dns/dynamic-service-host-name-server.xml.i delete mode 100644 interface-definitions/include/dns/dynamic-service-wait-expiry-time.xml.i delete mode 100644 interface-definitions/include/dns/dynamic-service-zone.xml.i 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 deleted file mode 100644 index 9dd14f97c..000000000 --- a/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i +++ /dev/null @@ -1,35 +0,0 @@ - - - - Hostname to register with Dynamic DNS service - - #include - (\@|\*)[-.A-Za-z0-9]* - - Host-name must be alphanumeric, can contain hyphens and can be prefixed with '@' or '*' - - - - - - Remote Dynamic DNS server to send updates to - - ipv4 - IPv4 address of the remote server - - - ipv6 - IPv6 address of the remote server - - - hostname - Fully qualified domain name of the remote server - - - - - - Remote server must be IP address or fully qualified domain name - - - 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 deleted file mode 100644 index 866690cbe..000000000 --- a/interface-definitions/include/dns/dynamic-service-wait-expiry-time.xml.i +++ /dev/null @@ -1,28 +0,0 @@ - - - - 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/interface-definitions/include/dns/dynamic-service-zone.xml.i b/interface-definitions/include/dns/dynamic-service-zone.xml.i deleted file mode 100644 index 0cc00468f..000000000 --- a/interface-definitions/include/dns/dynamic-service-zone.xml.i +++ /dev/null @@ -1,14 +0,0 @@ - - - - DNS zone to be updated - - txt - Name of DNS zone - - - - - - - -- cgit v1.2.3 From 06bf126a767c2775bc90021f0435b86f2bad9a5b Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 30 Nov 2023 19:16:53 -0600 Subject: ddclient: T5791: Migration script for dynamic dns config path change --- .../include/version/dns-dynamic-version.xml.i | 2 +- src/migration-scripts/dns-dynamic/2-to-3 | 85 ++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/migration-scripts/dns-dynamic/2-to-3 diff --git a/interface-definitions/include/version/dns-dynamic-version.xml.i b/interface-definitions/include/version/dns-dynamic-version.xml.i index 7bdb90a35..773a6ab51 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/src/migration-scripts/dns-dynamic/2-to-3 b/src/migration-scripts/dns-dynamic/2-to-3 new file mode 100644 index 000000000..02bc9324a --- /dev/null +++ b/src/migration-scripts/dns-dynamic/2-to-3 @@ -0,0 +1,85 @@ +#!/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 . + +# T5791: +# - migrate "service dns dynamic address web web-options ..." +# to "service dns dynamic name address web ..." (per service) +# - migrate "service dns dynamic address
rfc2136 ..." +# to "service dns dynamic name address protocol 'nsupdate'" +# - migrate "service dns dynamic address service ..." +# to "service dns dynamic name address ..." + +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'] +address_path = base_path + ['address'] +name_path = base_path + ['name'] + +if not config.exists(address_path): + # Nothing to do + sys.exit(0) + +# config.copy does not recursively create a path, so initialize the name path +if not config.exists(name_path): + config.set(name_path) + +for address in config.list_nodes(address_path): + + # Move web-option as a configuration in each service instead of top level web-option + if config.exists(address_path + [address, 'web-options']) and address == 'web': + for svc_type in ['service', 'rfc2136']: + if config.exists(address_path + [address, svc_type]): + for svc_cfg in config.list_nodes(address_path + [address, svc_type]): + config.copy(address_path + [address, 'web-options'], + address_path + [address, svc_type, svc_cfg, 'web-options']) + config.delete(address_path + [address, 'web-options']) + + for svc_type in ['service', 'rfc2136']: + if config.exists(address_path + [address, svc_type]): + # Move RFC2136 as service configuration, rename to avoid name conflict and set protocol to 'nsupdate' + if svc_type == 'rfc2136': + for rfc_cfg_old in config.list_nodes(address_path + [address, 'rfc2136']): + rfc_cfg_new = f'{rfc_cfg_old}-rfc2136' + config.rename(address_path + [address, 'rfc2136', rfc_cfg_old], rfc_cfg_new) + config.set(address_path + [address, 'rfc2136', rfc_cfg_new, 'protocol'], 'nsupdate') + + # Add address as config value in each service before moving the service path + # And then copy the services from 'address service ' to 'name ' + for svc_cfg in config.list_nodes(address_path + [address, svc_type]): + config.set(address_path + [address, svc_type, svc_cfg, 'address'], address) + config.copy(address_path + [address, svc_type, svc_cfg], name_path + [svc_cfg]) + +# Finally cleanup the old address path +config.delete(address_path) + +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 706528f53af73ab15b1b2841f68250cb14bbcca0 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 29 Nov 2023 15:02:17 -0600 Subject: ddclient: T5791: Update smoketest for dynamic dns config path change --- smoketest/scripts/cli/test_service_dns_dynamic.py | 130 +++++++++++----------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index fe213a8ae..cb3d90593 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -32,6 +32,7 @@ DDCLIENT_PID = '/run/ddclient/ddclient.pid' DDCLIENT_PNAME = 'ddclient' base_path = ['service', 'dns', 'dynamic'] +name_path = base_path + ['name'] server = 'ddns.vyos.io' hostname = 'test.ddns.vyos.io' zone = 'vyos.io' @@ -58,38 +59,38 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv4 standard DDNS service configuration def test_01_dyndns_service_standard(self): - svc_path = ['address', interface, 'service'] services = {'cloudflare': {'protocol': 'cloudflare'}, 'freedns': {'protocol': 'freedns', 'username': username}, 'zoneedit': {'protocol': 'zoneedit1', 'username': username}} for svc, details in services.items(): - 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]) + self.cli_set(name_path + [svc, 'address', interface]) + self.cli_set(name_path + [svc, 'host-name', hostname]) + self.cli_set(name_path + [svc, 'password', password]) + self.cli_set(name_path + [svc, 'zone', zone]) + self.cli_set(name_path + [svc, 'ttl', ttl]) for opt, value in details.items(): - self.cli_set(base_path + svc_path + [svc, opt, value]) + self.cli_set(name_path + [svc, opt, value]) # 'zone' option is supported and required by 'cloudfare', but not 'freedns' and 'zoneedit' - self.cli_set(base_path + svc_path + [svc, 'zone', zone]) + self.cli_set(name_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 + svc_path + [svc, 'zone']) + self.cli_delete(name_path + [svc, 'zone']) # 'ttl' option is supported by 'cloudfare', but not 'freedns' and 'zoneedit' - self.cli_set(base_path + svc_path + [svc, 'ttl', ttl]) + self.cli_set(name_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 + svc_path + [svc, 'ttl']) + self.cli_delete(name_path + [svc, 'ttl']) # commit changes self.cli_commit() @@ -113,7 +114,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv6 only DDNS service configuration def test_02_dyndns_service_ipv6(self): interval = '60' - svc_path = ['address', interface, 'service', 'dynv6'] + svc_path = name_path + ['dynv6'] proto = 'dyndns2' ip_version = 'ipv6' wait_time = '600' @@ -121,19 +122,20 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): expiry_time_bad = '360' 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]) - 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]) + self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['ip-version', ip_version]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['server', server]) + self.cli_set(svc_path + ['username', username]) + self.cli_set(svc_path + ['password', password]) + self.cli_set(svc_path + ['host-name', hostname]) + self.cli_set(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_set(svc_path + ['expiry-time', expiry_time_bad]) self.cli_commit() - self.cli_set(base_path + svc_path + ['expiry-time', expiry_time_good]) + self.cli_set(svc_path + ['expiry-time', expiry_time_good]) # commit changes self.cli_commit() @@ -152,25 +154,25 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv4+IPv6 dual DDNS service configuration def test_03_dyndns_service_dual_stack(self): - 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 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]) + self.cli_set(name_path + [name, 'address', interface]) + self.cli_set(name_path + [name, 'host-name', hostname]) + self.cli_set(name_path + [name, 'password', password]) for opt, value in details.items(): - self.cli_set(base_path + svc_path + [name, opt, value]) + self.cli_set(name_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]) + self.cli_set(name_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']) + self.cli_delete(name_path + [name, 'ip-version']) # commit changes self.cli_commit() @@ -197,16 +199,19 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def test_04_dyndns_rfc2136(self): # Check if DDNS service can be configured and runs - svc_path = ['address', interface, 'rfc2136', 'vyos'] + svc_path = name_path + ['vyos'] + proto = 'nsupdate' with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file: key_file.write(b'S3cretKey') - 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]) + self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['server', server]) + self.cli_set(svc_path + ['zone', zone]) + self.cli_set(svc_path + ['key', key_file.name]) + self.cli_set(svc_path + ['ttl', ttl]) + self.cli_set(svc_path + ['host-name', hostname]) # commit changes self.cli_commit() @@ -215,7 +220,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') self.assertIn(f'use=if', ddclient_conf) self.assertIn(f'if={interface}', ddclient_conf) - self.assertIn(f'protocol=nsupdate', ddclient_conf) + self.assertIn(f'protocol={proto}', 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) @@ -223,16 +228,17 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def test_05_dyndns_hostname(self): # Check if DDNS service can be configured and runs - svc_path = ['address', interface, 'service', 'namecheap'] + svc_path = name_path + ['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]) + self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['server', server]) + self.cli_set(svc_path + ['username', username]) + self.cli_set(svc_path + ['password', password]) + self.cli_set(svc_path + ['host-name', name]) # commit changes self.cli_commit() @@ -247,42 +253,32 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): 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'] + svc_path = name_path + ['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]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['zone', zone]) + self.cli_set(svc_path + ['password', password]) + self.cli_set(svc_path + ['host-name', hostname]) + self.cli_set(svc_path + ['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_set(svc_path + ['address', interface]) self.cli_commit() - self.cli_delete(base_path_iface + ['web-options']) + self.cli_set(svc_path + ['address', 'web']) # 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]) + with self.assertRaises(ConfigSessionError): + self.cli_set(svc_path + ['web-options', 'url', web_url_bad]) + self.cli_commit() + self.cli_set(svc_path + ['web-options', 'url', web_url_good]) # commit changes self.cli_commit() @@ -300,15 +296,17 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # 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'] + svc_path = name_path + ['cloudflare'] + proto = 'cloudflare' 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']) - 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]) + self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['host-name', hostname]) + self.cli_set(svc_path + ['zone', zone]) + self.cli_set(svc_path + ['password', password]) # commit changes self.cli_commit() -- cgit v1.2.3 From 5674a29374ad0a2a0d3634ceabd7db6932a9f1ab Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 3 Dec 2023 02:41:54 -0600 Subject: ddclient: T5791: Simplify and fix migration script for dynamic dns Mark 'dns dynamic name' as tag node to avoid unexpected nesting. Also, fix file exec permission for migration script. --- src/migration-scripts/dns-dynamic/2-to-3 | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) mode change 100644 => 100755 src/migration-scripts/dns-dynamic/2-to-3 diff --git a/src/migration-scripts/dns-dynamic/2-to-3 b/src/migration-scripts/dns-dynamic/2-to-3 old mode 100644 new mode 100755 index 02bc9324a..187c2a895 --- a/src/migration-scripts/dns-dynamic/2-to-3 +++ b/src/migration-scripts/dns-dynamic/2-to-3 @@ -44,35 +44,38 @@ if not config.exists(address_path): # Nothing to do sys.exit(0) -# config.copy does not recursively create a path, so initialize the name path +# config.copy does not recursively create a path, so initialize the name path as tagged node if not config.exists(name_path): config.set(name_path) + config.set_tag(name_path) for address in config.list_nodes(address_path): + address_path_tag = address_path + [address] + # Move web-option as a configuration in each service instead of top level web-option - if config.exists(address_path + [address, 'web-options']) and address == 'web': + if config.exists(address_path_tag + ['web-options']) and address == 'web': for svc_type in ['service', 'rfc2136']: - if config.exists(address_path + [address, svc_type]): - for svc_cfg in config.list_nodes(address_path + [address, svc_type]): - config.copy(address_path + [address, 'web-options'], - address_path + [address, svc_type, svc_cfg, 'web-options']) - config.delete(address_path + [address, 'web-options']) + if config.exists(address_path_tag + [svc_type]): + for svc_cfg in config.list_nodes(address_path_tag + [svc_type]): + config.copy(address_path_tag + ['web-options'], + address_path_tag + [svc_type, svc_cfg, 'web-options']) + config.delete(address_path_tag + ['web-options']) for svc_type in ['service', 'rfc2136']: - if config.exists(address_path + [address, svc_type]): + if config.exists(address_path_tag + [svc_type]): # Move RFC2136 as service configuration, rename to avoid name conflict and set protocol to 'nsupdate' if svc_type == 'rfc2136': - for rfc_cfg_old in config.list_nodes(address_path + [address, 'rfc2136']): + for rfc_cfg_old in config.list_nodes(address_path_tag + ['rfc2136']): rfc_cfg_new = f'{rfc_cfg_old}-rfc2136' - config.rename(address_path + [address, 'rfc2136', rfc_cfg_old], rfc_cfg_new) - config.set(address_path + [address, 'rfc2136', rfc_cfg_new, 'protocol'], 'nsupdate') + config.rename(address_path_tag + ['rfc2136', rfc_cfg_old], rfc_cfg_new) + config.set(address_path_tag + ['rfc2136', rfc_cfg_new, 'protocol'], 'nsupdate') # Add address as config value in each service before moving the service path # And then copy the services from 'address service ' to 'name ' - for svc_cfg in config.list_nodes(address_path + [address, svc_type]): - config.set(address_path + [address, svc_type, svc_cfg, 'address'], address) - config.copy(address_path + [address, svc_type, svc_cfg], name_path + [svc_cfg]) + for svc_cfg in config.list_nodes(address_path_tag + [svc_type]): + config.set(address_path_tag + [svc_type, svc_cfg, 'address'], address) + config.copy(address_path_tag + [svc_type, svc_cfg], name_path + [svc_cfg]) # Finally cleanup the old address path config.delete(address_path) -- cgit v1.2.3