diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index f089f0e52..388e7c5d2 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -1,208 +1,212 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="service"> <children> <node name="dns"> <properties> <help>Domain Name System (DNS) related services</help> </properties> <children> <node name="dynamic" owner="${vyos_conf_scripts_dir}/dns_dynamic.py"> <properties> <help>Dynamic DNS</help> </properties> <children> <tagNode name="name"> <properties> <help>Dynamic DNS configuration</help> <valueHelp> <format>txt</format> <description>Dynamic DNS service name</description> </valueHelp> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> + </constraint> + <constraintErrorMessage>Dynamic DNS service name must be alphanumeric and can contain hyphens and underscores</constraintErrorMessage> </properties> <children> #include <include/generic-description.xml.i> <leafNode name="protocol"> <properties> <help>ddclient protocol used for Dynamic DNS service</help> <completionHelp> <script>${vyos_completion_dir}/list_ddclient_protocols.sh</script> </completionHelp> <constraint> <validator name="ddclient-protocol"/> </constraint> </properties> </leafNode> <leafNode name="address"> <properties> <help>Obtain IP address to send Dynamic DNS update for</help> <valueHelp> <format>txt</format> <description>Use interface to obtain the IP address</description> </valueHelp> <valueHelp> <format>web</format> <description>Use HTTP(S) web request to obtain the IP address</description> </valueHelp> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> <list>web</list> </completionHelp> <constraint> #include <include/constraint/interface-name.xml.i> <regex>web</regex> </constraint> </properties> </leafNode> <node name="web-options"> <properties> <help>Options when using HTTP(S) web request to obtain the IP address</help> </properties> <children> #include <include/url-http-https.xml.i> <leafNode name="skip"> <properties> <help>Pattern to skip from the HTTP(S) respose</help> <valueHelp> <format>txt</format> <description>Pattern to skip from the HTTP(S) respose to extract the external IP address</description> </valueHelp> </properties> </leafNode> </children> </node> <leafNode name="ip-version"> <properties> <help>IP address version to use</help> <valueHelp> <format>_ipv4</format> <description>Use only IPv4 address</description> </valueHelp> <valueHelp> <format>_ipv6</format> <description>Use only IPv6 address</description> </valueHelp> <valueHelp> <format>both</format> <description>Use both IPv4 and IPv6 address</description> </valueHelp> <completionHelp> <list>ipv4 ipv6 both</list> </completionHelp> <constraint> <regex>(ipv[46]|both)</regex> </constraint> <constraintErrorMessage>IP Version must be literal 'ipv4', 'ipv6' or 'both'</constraintErrorMessage> </properties> <defaultValue>ipv4</defaultValue> </leafNode> <leafNode name="host-name"> <properties> <help>Hostname to register with Dynamic DNS service</help> <constraint> #include <include/constraint/host-name.xml.i> <regex>(\@|\*)[-.A-Za-z0-9]*</regex> </constraint> <constraintErrorMessage>Host-name must be alphanumeric, can contain hyphens and can be prefixed with '@' or '*'</constraintErrorMessage> <multi/> </properties> </leafNode> <leafNode name="server"> <properties> <help>Remote Dynamic DNS server to send updates to</help> <valueHelp> <format>ipv4</format> <description>IPv4 address of the remote server</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>IPv6 address of the remote server</description> </valueHelp> <valueHelp> <format>hostname</format> <description>Fully qualified domain name of the remote server</description> </valueHelp> <constraint> <validator name="ip-address"/> <validator name="fqdn"/> </constraint> <constraintErrorMessage>Remote server must be IP address or fully qualified domain name</constraintErrorMessage> </properties> </leafNode> <leafNode name="zone"> <properties> <help>DNS zone to be updated</help> <valueHelp> <format>txt</format> <description>Name of DNS zone</description> </valueHelp> <constraint> <validator name="fqdn"/> </constraint> </properties> </leafNode> #include <include/generic-username.xml.i> #include <include/generic-password.xml.i> <leafNode name="key"> <properties> <help>File containing TSIG authentication key for RFC2136 nsupdate on remote DNS server</help> <valueHelp> <format>filename</format> <description>File in /config/auth directory</description> </valueHelp> <constraint> <validator name="file-path" argument="--strict --parent-dir /config/auth"/> </constraint> </properties> </leafNode> #include <include/dns/time-to-live.xml.i> <leafNode name="wait-time"> <properties> <help>Time in seconds to wait between update attempts</help> <valueHelp> <format>u32:60-86400</format> <description>Time in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 60-86400"/> </constraint> <constraintErrorMessage>Wait time must be between 60 and 86400 seconds</constraintErrorMessage> </properties> </leafNode> <leafNode name="expiry-time"> <properties> <help>Time in seconds for the hostname to be marked expired in cache</help> <valueHelp> <format>u32:300-2160000</format> <description>Time in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 300-2160000"/> </constraint> <constraintErrorMessage>Expiry time must be between 300 and 2160000 seconds</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> <leafNode name="interval"> <properties> <help>Interval in seconds to wait between Dynamic DNS updates</help> <valueHelp> <format>u32:60-3600</format> <description>Time in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 60-3600"/> </constraint> <constraintErrorMessage>Interval must be between 60 and 3600 seconds</constraintErrorMessage> </properties> <defaultValue>300</defaultValue> </leafNode> #include <include/interface/vrf.xml.i> </children> </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/src/migration-scripts/dns-dynamic/0-to-1 b/src/migration-scripts/dns-dynamic/0-to-1 index d80e8d44a..4f6083eab 100755 --- a/src/migration-scripts/dns-dynamic/0-to-1 +++ b/src/migration-scripts/dns-dynamic/0-to-1 @@ -1,104 +1,117 @@ #!/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 <http://www.gnu.org/licenses/>. # T5144: # - migrate "service dns dynamic interface ..." # to "service dns dynamic address ..." # - migrate "service dns dynamic interface <interface> use-web ..." # to "service dns dynamic address <address> web-options ..." # - migrate "service dns dynamic interface <interface> rfc2136 <config> record ..." # to "service dns dynamic address <address> rfc2136 <config> host-name ..." # - migrate "service dns dynamic interface <interface> service <config> login ..." # to "service dns dynamic address <address> service <config> username ..." # - apply global 'ipv6-enable' to per <config> 'ip-version: ipv6' # - apply service protocol mapping upfront, they are not 'auto-detected' anymore import sys from vyos.configtree import ConfigTree service_protocol_mapping = { 'afraid': 'freedns', 'changeip': 'changeip', 'cloudflare': 'cloudflare', 'dnspark': 'dnspark', 'dslreports': 'dslreports1', 'dyndns': 'dyndns2', 'easydns': 'easydns', 'namecheap': 'namecheap', 'noip': 'noip', 'sitelutions': 'sitelutions', 'zoneedit': 'zoneedit1' } 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) old_base_path = ['service', 'dns', 'dynamic', 'interface'] new_base_path = ['service', 'dns', 'dynamic', 'address'] if not config.exists(old_base_path): # Nothing to do sys.exit(0) # Migrate "service dns dynamic interface" # to "service dns dynamic address" config.rename(old_base_path, new_base_path[-1]) for address in config.list_nodes(new_base_path): # Migrate "service dns dynamic interface <interface> rfc2136 <config> record" # to "service dns dynamic address <address> rfc2136 <config> host-name" if config.exists(new_base_path + [address, 'rfc2136']): for rfc_cfg in config.list_nodes(new_base_path + [address, 'rfc2136']): if config.exists(new_base_path + [address, 'rfc2136', rfc_cfg, 'record']): config.rename(new_base_path + [address, 'rfc2136', rfc_cfg, 'record'], 'host-name') # Migrate "service dns dynamic interface <interface> service <config> login" # to "service dns dynamic address <address> service <config> username" if config.exists(new_base_path + [address, 'service']): for svc_cfg in config.list_nodes(new_base_path + [address, 'service']): if config.exists(new_base_path + [address, 'service', svc_cfg, 'login']): config.rename(new_base_path + [address, 'service', svc_cfg, 'login'], 'username') # Apply global 'ipv6-enable' to per <config> 'ip-version: ipv6' if config.exists(new_base_path + [address, 'ipv6-enable']): - config.set(new_base_path + [address, 'service', svc_cfg, 'ip-version'], - value='ipv6', replace=False) + config.set(new_base_path + [address, 'service', svc_cfg, 'ip-version'], 'ipv6') config.delete(new_base_path + [address, 'ipv6-enable']) # Apply service protocol mapping upfront, they are not 'auto-detected' anymore if svc_cfg in service_protocol_mapping: config.set(new_base_path + [address, 'service', svc_cfg, 'protocol'], - value=service_protocol_mapping.get(svc_cfg), replace=False) + service_protocol_mapping.get(svc_cfg)) - # Migrate "service dns dynamic interface <interface> use-web" - # to "service dns dynamic address <address> web-options" - # Also, rename <address> to 'web' literal for backward compatibility + # If use-web is set, then: + # Move "service dns dynamic address <address> <service|rfc2136> <service> ..." + # to "service dns dynamic address web <service|rfc2136> <service>-<address> ..." + # Move "service dns dynamic address web use-web ..." + # to "service dns dynamic address web web-options ..." + # Note: The config is named <service>-<address> to avoid name conflict with old entries if config.exists(new_base_path + [address, 'use-web']): - config.rename(new_base_path + [address], 'web') - config.rename(new_base_path + ['web', 'use-web'], 'web-options') + for svc_type in ['rfc2136', 'service']: + if config.exists(new_base_path + [address, svc_type]): + config.set(new_base_path + ['web', svc_type]) + config.set_tag(new_base_path + ['web', svc_type]) + for svc_cfg in config.list_nodes(new_base_path + [address, svc_type]): + config.copy(new_base_path + [address, svc_type, svc_cfg], + new_base_path + ['web', svc_type, f'{svc_cfg}-{address}']) + + # Multiple web-options were not supported, so copy only the first one + if not config.exists(new_base_path + ['web', 'web-options']): + config.copy(new_base_path + [address, 'use-web'], new_base_path + ['web', 'web-options']) + + config.delete(new_base_path + [address]) 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) diff --git a/src/migration-scripts/dns-dynamic/2-to-3 b/src/migration-scripts/dns-dynamic/2-to-3 index 187c2a895..e5910f7b4 100755 --- a/src/migration-scripts/dns-dynamic/2-to-3 +++ b/src/migration-scripts/dns-dynamic/2-to-3 @@ -1,88 +1,119 @@ #!/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 <http://www.gnu.org/licenses/>. # T5791: # - migrate "service dns dynamic address web web-options ..." # to "service dns dynamic name <service> address web ..." (per service) # - migrate "service dns dynamic address <address> rfc2136 <service> ..." # to "service dns dynamic name <service> address <interface> protocol 'nsupdate'" # - migrate "service dns dynamic address <interface> service <service> ..." # to "service dns dynamic name <service> address <interface> ..." +# - normalize the all service names to conform with name constraints import sys +import re +from unicodedata import normalize from vyos.configtree import ConfigTree +def normalize_name(name): + """Normalize service names to conform with name constraints. + + This is necessary as part of migration because there were no constraints in + the old name format. + """ + # Normalize unicode characters to ASCII (NFKD) + # Replace all separators with hypens, strip leading and trailing hyphens + name = normalize('NFKD', name).encode('ascii', 'ignore').decode() + name = re.sub(r'(\s|\W)+', '-', name).strip('-') + + return name + + 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 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_tag + ['web-options']) and address == 'web': for svc_type in ['service', 'rfc2136']: 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_tag + [svc_type]): - # Move RFC2136 as service configuration, rename to avoid name conflict and set protocol to 'nsupdate' + # Set protocol to 'nsupdate' for RFC2136 configuration if svc_type == '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_tag + ['rfc2136', rfc_cfg_old], rfc_cfg_new) - config.set(address_path_tag + ['rfc2136', rfc_cfg_new, 'protocol'], 'nsupdate') + for rfc_cfg in config.list_nodes(address_path_tag + ['rfc2136']): + config.set(address_path_tag + ['rfc2136', rfc_cfg, 'protocol'], 'nsupdate') # Add address as config value in each service before moving the service path - # And then copy the services from 'address <interface> service <service>' to 'name <service>' + # And then copy the services from 'address <interface> service <service>' + # to 'name (service|rfc2136)-<service>-<address>' + # Note: The new service is named (service|rfc2136)-<service>-<address> + # to avoid name conflict with old entries 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]) + config.copy(address_path_tag + [svc_type, svc_cfg], + name_path + ['-'.join([svc_type, svc_cfg, address])]) # Finally cleanup the old address path config.delete(address_path) +# Normalize the all service names to conform with name constraints +index = 1 +for name in config.list_nodes(name_path): + new_name = normalize_name(name) + if new_name != name: + # Append index if there is still a name conflicts after normalization + # For example, "foo-?(" and "foo-!)" both normalize to "foo-" + if config.exists(name_path + [new_name]): + new_name = f'{new_name}-{index}' + index += 1 + config.rename(name_path + [name], new_name) + 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)