diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 081f7ed42e..8aaeeb29d5 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -284,11 +284,11 @@ - Name of static mapping + Hostname for static mapping reservation - [-_a-zA-Z0-9.]+ + - Invalid static mapping name, may only be alphanumeric, dot and hyphen + Invalid static mapping hostname #include @@ -304,18 +304,8 @@ - - - Media Access Control (MAC) address - - macaddr - Hardware (MAC) address - - - - - - + #include + #include diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in index b37f794348..10fdbf3f72 100644 --- a/interface-definitions/dhcpv6-server.xml.in +++ b/interface-definitions/dhcpv6-server.xml.in @@ -301,27 +301,16 @@ - Name of static mapping + Hostname for static mapping reservation - [-_a-zA-Z0-9.]+ + - Invalid static mapping name. May only contain letters, numbers and .-_ + Invalid static mapping hostname #include - - - Client identifier (DUID) for this static mapping - - h[[:h]...] - DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id) - - - ([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2}) - - Invalid DUID, must be in the format h[[:h]...] - - + #include + #include Client IPv6 address for this static mapping diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i index 7c4b5633e3..cc84ea8b9a 100644 --- a/interface-definitions/include/version/dhcp-server-version.xml.i +++ b/interface-definitions/include/version/dhcp-server-version.xml.i @@ -1,3 +1,3 @@ - + diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i index ae4178c903..cb026a54a2 100644 --- a/interface-definitions/include/version/dhcpv6-server-version.xml.i +++ b/interface-definitions/include/version/dhcpv6-server-version.xml.i @@ -1,3 +1,3 @@ - + diff --git a/python/vyos/kea.py b/python/vyos/kea.py index 4a517da5f3..819fe16a98 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -121,14 +121,20 @@ def kea_parse_subnet(subnet, config): if 'disable' in host_config: continue - obj = { - 'hw-address': host_config['mac_address'] + reservation = { + 'hostname': host, } + if 'mac' in host_config: + reservation['hw-address'] = host_config['mac'] + + if 'duid' in host_config: + reservation['duid'] = host_config['duid'] + if 'ip_address' in host_config: - obj['ip-address'] = host_config['ip_address'] + reservation['ip-address'] = host_config['ip_address'] - reservations.append(obj) + reservations.append(reservation) out['reservations'] = reservations unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller') @@ -178,7 +184,7 @@ def kea6_parse_options(config): if addrs: options.append({'name': 'sip-server-addr', 'data': ", ".join(addrs)}) - + if hosts: options.append({'name': 'sip-server-dns', 'data': ", ".join(hosts)}) @@ -234,10 +240,15 @@ def kea6_parse_subnet(subnet, config): if 'disable' in host_config: continue - reservation = {} + reservation = { + 'hostname': host + } + + if 'mac' in host_config: + reservation['hw-address'] = host_config['mac'] - if 'identifier' in host_config: - reservation['duid'] = host_config['identifier'] + if 'duid' in host_config: + reservation['duid'] = host_config['duid'] if 'ipv6_address' in host_config: reservation['ip-addresses'] = [ host_config['ipv6_address'] ] @@ -305,7 +316,7 @@ def kea_get_active_config(inet): ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket' config = _ctrl_socket_command(ctrl_socket, 'config-get') - + if not config or 'result' not in config or config['result'] != 0: return None diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 9f6e05ff39..bf0c09965c 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -22,13 +22,10 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.utils.dict import dict_search_recursive from vyos.utils.process import process_named_running from vyos.utils.file import read_file -from vyos.template import address_from_cidr from vyos.template import inc_ip from vyos.template import dec_ip -from vyos.template import netmask_from_cidr PROCESS_NAME = 'kea-dhcp4' CTRL_PROCESS_NAME = 'kea-ctrl-agent' @@ -45,6 +42,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestServiceDHCPServer, cls).setUpClass() + # Clear out current configuration to allow running this test on a live system + cls.cli_delete(cls, base_path) cidr_mask = subnet.split('/')[-1] cls.cli_set(cls, ['interfaces', 'dummy', 'dum8765', 'address', f'{router}/{cidr_mask}']) @@ -300,10 +299,16 @@ def test_dhcp_single_pool_static_mapping(self): client_base = 10 for client in ['client1', 'client2', 'client3']: mac = '00:50:00:00:00:{}'.format(client_base) - self.cli_set(pool + ['static-mapping', client, 'mac-address', mac]) + self.cli_set(pool + ['static-mapping', client, 'mac', mac]) self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)]) client_base += 1 + # cannot have both mac-address and duid set + with self.assertRaises(ConfigSessionError): + self.cli_set(pool + ['static-mapping', 'client1', 'duid', '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11']) + self.cli_commit() + self.cli_delete(pool + ['static-mapping', 'client1', 'duid']) + # commit changes self.cli_commit() @@ -337,7 +342,7 @@ def test_dhcp_single_pool_static_mapping(self): self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'], - {'hw-address': mac, 'ip-address': ip}) + {'hostname': client, 'hw-address': mac, 'ip-address': ip}) client_base += 1 @@ -373,7 +378,7 @@ def test_dhcp_multiple_pools(self): client_base = 60 for client in ['client1', 'client2', 'client3', 'client4']: mac = '02:50:00:00:00:{}'.format(client_base) - self.cli_set(pool + ['static-mapping', client, 'mac-address', mac]) + self.cli_set(pool + ['static-mapping', client, 'mac', mac]) self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)]) client_base += 1 @@ -429,7 +434,7 @@ def test_dhcp_multiple_pools(self): self.verify_config_object( obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'], - {'hw-address': mac, 'ip-address': ip}) + {'hostname': client, 'hw-address': mac, 'ip-address': ip}) client_base += 1 diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 175a675372..f163cc69a1 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -41,6 +41,9 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestServiceDHCPv6Server, cls).setUpClass() + # Clear out current configuration to allow running this test on a live system + cls.cli_delete(cls, base_path) + cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr]) @classmethod @@ -122,12 +125,18 @@ def test_single_pool(self): client_base = 1 for client in ['client1', 'client2', 'client3']: - cid = '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{}'.format(client_base) - self.cli_set(pool + ['static-mapping', client, 'identifier', cid]) + duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}' + self.cli_set(pool + ['static-mapping', client, 'duid', duid]) self.cli_set(pool + ['static-mapping', client, 'ipv6-address', inc_ip(subnet, client_base)]) self.cli_set(pool + ['static-mapping', client, 'ipv6-prefix', inc_ip(subnet, client_base << 64) + '/64']) client_base += 1 + # cannot have both mac-address and duid set + with self.assertRaises(ConfigSessionError): + self.cli_set(pool + ['static-mapping', 'client1', 'mac', '00:50:00:00:00:11']) + self.cli_commit() + self.cli_delete(pool + ['static-mapping', 'client1', 'mac']) + # commit changes self.cli_commit() @@ -182,14 +191,14 @@ def test_single_pool(self): client_base = 1 for client in ['client1', 'client2', 'client3']: - cid = '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{}'.format(client_base) + duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}' ip = inc_ip(subnet, client_base) prefix = inc_ip(subnet, client_base << 64) + '/64' self.verify_config_object( obj, ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'reservations'], - {'duid': cid, 'ip-addresses': [ip], 'prefixes': [prefix]}) + {'hostname': client, 'duid': duid, 'ip-addresses': [ip], 'prefixes': [prefix]}) client_base += 1 diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index e06a3c8a8d..c1308cda78 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -18,7 +18,6 @@ from ipaddress import ip_address from ipaddress import ip_network -from netaddr import IPAddress from netaddr import IPRange from sys import exit @@ -142,7 +141,7 @@ def get_config(config=None): {'range' : new_range_dict}) if dict_search('failover.certificate', dhcp): - dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) return dhcp @@ -227,9 +226,10 @@ def verify(dhcp): raise ConfigError(f'Configured static lease address for mapping "{mapping}" is\n' \ f'not within shared-network "{network}, {subnet}"!') - if 'mac_address' not in mapping_config: - raise ConfigError(f'MAC address required for static mapping "{mapping}"\n' \ - f'within shared-network "{network}, {subnet}"!') + if ('mac' not in mapping_config and 'duid' not in mapping_config) or \ + ('mac' in mapping_config and 'duid' in mapping_config): + raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for ' + f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!') # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index b01f510e5d..f9da3d84a9 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -135,6 +135,11 @@ def verify(dhcpv6): if ip_address(mapping_config['ipv6_address']) not in ip_network(subnet): raise ConfigError(f'static-mapping address for mapping "{mapping}" is not in subnet "{subnet}"!') + if ('mac' not in mapping_config and 'duid' not in mapping_config) or \ + ('mac' in mapping_config and 'duid' in mapping_config): + raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for ' + f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!') + if 'vendor_option' in subnet_config: if len(dict_search('vendor_option.cisco.tftp_server', subnet_config)) > 2: raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!') diff --git a/src/migration-scripts/dhcp-server/7-to-8 b/src/migration-scripts/dhcp-server/7-to-8 new file mode 100755 index 0000000000..151aa6d7b9 --- /dev/null +++ b/src/migration-scripts/dhcp-server/7-to-8 @@ -0,0 +1,65 @@ +#!/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 . + +# T3316: +# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore) +# - Rename "service dhcp-server shared-network-name ... static-mapping mac-address ..." +# to "service dhcp-server shared-network-name ... static-mapping mac ..." + +import sys +import re +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() + +base = ['service', 'dhcp-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + sys.exit(0) + +for network in config.list_nodes(base): + # Run this for every specified 'subnet' + if config.exists(base + [network, 'subnet']): + for subnet in config.list_nodes(base + [network, 'subnet']): + base_subnet = base + [network, 'subnet', subnet] + if config.exists(base_subnet + ['static-mapping']): + for hostname in config.list_nodes(base_subnet + ['static-mapping']): + base_mapping = base_subnet + ['static-mapping', hostname] + + # Rename the 'mac-address' node to 'mac' + if config.exists(base_mapping + ['mac-address']): + config.rename(base_mapping + ['mac-address'], 'mac') + + # Adjust hostname to have valid FQDN characters only + new_hostname = re.sub(r'[^a-zA-Z0-9-.]', '-', hostname) + if new_hostname != hostname: + config.rename(base_mapping, new_hostname) + +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)) + exit(1) diff --git a/src/migration-scripts/dhcpv6-server/2-to-3 b/src/migration-scripts/dhcpv6-server/2-to-3 new file mode 100755 index 0000000000..f4bdc1d1e6 --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/2-to-3 @@ -0,0 +1,78 @@ +#!/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 . + +# T3316: +# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore) +# - Adjust duid (old identifier) to comply with duid format +# - Rename "service dhcpv6-server shared-network-name ... static-mapping identifier ..." +# to "service dhcpv6-server shared-network-name ... static-mapping duid ..." +# - Rename "service dhcpv6-server shared-network-name ... static-mapping mac-address ..." +# to "service dhcpv6-server shared-network-name ... static-mapping mac ..." + +import sys +import re +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() + +base = ['service', 'dhcpv6-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + sys.exit(0) + +for network in config.list_nodes(base): + # Run this for every specified 'subnet' + if config.exists(base + [network, 'subnet']): + for subnet in config.list_nodes(base + [network, 'subnet']): + base_subnet = base + [network, 'subnet', subnet] + if config.exists(base_subnet + ['static-mapping']): + for hostname in config.list_nodes(base_subnet + ['static-mapping']): + base_mapping = base_subnet + ['static-mapping', hostname] + if config.exists(base_mapping + ['identifier']): + + # Adjust duid to comply with duid format (a:3:b:04:... => 0a:03:0b:04:...) + duid = config.return_value(base_mapping + ['identifier']) + new_duid = ':'.join(x.rjust(2,'0') for x in duid.split(':')) + if new_duid != duid: + config.set(base_mapping + ['identifier'], new_duid) + + # Rename the 'identifier' node to 'duid' + config.rename(base_mapping + ['identifier'], 'duid') + + # Rename the 'mac-address' node to 'mac' + if config.exists(base_mapping + ['mac-address']): + config.rename(base_mapping + ['mac-address'], 'mac') + + # Adjust hostname to have valid FQDN characters only + new_hostname = re.sub(r'[^a-zA-Z0-9-.]', '-', hostname) + if new_hostname != hostname: + config.rename(base_mapping, new_hostname) + +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)) + exit(1)