diff options
author | Christian Breunig <christian@breunig.cc> | 2023-12-28 13:07:41 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-28 13:07:41 +0100 |
commit | b0fc2507aab533c1d6c4472667f39f9c11f40e69 (patch) | |
tree | af1adf71a752cb3cdfd8a7f5dfe2d977ddd0bd7a | |
parent | 957c99f20f9c08b0fc5dd51664a06cae16bc52c1 (diff) | |
parent | 8ed44ecef860601ff82e4c5102fd11e88fe040c5 (diff) | |
download | vyos-1x-b0fc2507aab533c1d6c4472667f39f9c11f40e69.tar.gz vyos-1x-b0fc2507aab533c1d6c4472667f39f9c11f40e69.zip |
Merge pull request #2650 from indrajitr/kea-reservation-fix
dhcp: T3316: Support hostname, DUID and MAC address in reservation
-rw-r--r-- | interface-definitions/dhcp-server.xml.in | 20 | ||||
-rw-r--r-- | interface-definitions/dhcpv6-server.xml.in | 21 | ||||
-rw-r--r-- | interface-definitions/include/version/dhcp-server-version.xml.i | 2 | ||||
-rw-r--r-- | interface-definitions/include/version/dhcpv6-server-version.xml.i | 2 | ||||
-rw-r--r-- | python/vyos/kea.py | 29 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_dhcp-server.py | 19 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_dhcpv6-server.py | 17 | ||||
-rwxr-xr-x | src/conf_mode/dhcp_server.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/dhcpv6_server.py | 5 | ||||
-rwxr-xr-x | src/migration-scripts/dhcp-server/7-to-8 | 65 | ||||
-rwxr-xr-x | src/migration-scripts/dhcpv6-server/2-to-3 | 78 |
11 files changed, 210 insertions, 58 deletions
diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 081f7ed42..8aaeeb29d 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -284,11 +284,11 @@ </tagNode> <tagNode name="static-mapping"> <properties> - <help>Name of static mapping</help> + <help>Hostname for static mapping reservation</help> <constraint> - <regex>[-_a-zA-Z0-9.]+</regex> + <validator name="fqdn"/> </constraint> - <constraintErrorMessage>Invalid static mapping name, may only be alphanumeric, dot and hyphen</constraintErrorMessage> + <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage> </properties> <children> #include <include/generic-disable-node.xml.i> @@ -304,18 +304,8 @@ </constraint> </properties> </leafNode> - <leafNode name="mac-address"> - <properties> - <help>Media Access Control (MAC) address</help> - <valueHelp> - <format>macaddr</format> - <description>Hardware (MAC) address</description> - </valueHelp> - <constraint> - <validator name="mac-address"/> - </constraint> - </properties> - </leafNode> + #include <include/interface/mac.xml.i> + #include <include/interface/duid.xml.i> </children> </tagNode> <tagNode name="static-route"> diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in index b37f79434..10fdbf3f7 100644 --- a/interface-definitions/dhcpv6-server.xml.in +++ b/interface-definitions/dhcpv6-server.xml.in @@ -301,27 +301,16 @@ </leafNode> <tagNode name="static-mapping"> <properties> - <help>Name of static mapping</help> + <help>Hostname for static mapping reservation</help> <constraint> - <regex>[-_a-zA-Z0-9.]+</regex> + <validator name="fqdn"/> </constraint> - <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage> + <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage> </properties> <children> #include <include/generic-disable-node.xml.i> - <leafNode name="identifier"> - <properties> - <help>Client identifier (DUID) for this static mapping</help> - <valueHelp> - <format>h[[:h]...]</format> - <description>DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id)</description> - </valueHelp> - <constraint> - <regex>([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})</regex> - </constraint> - <constraintErrorMessage>Invalid DUID, must be in the format h[[:h]...]</constraintErrorMessage> - </properties> - </leafNode> + #include <include/interface/mac.xml.i> + #include <include/interface/duid.xml.i> <leafNode name="ipv6-address"> <properties> <help>Client IPv6 address for this static mapping</help> diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i index 7c4b5633e..cc84ea8b9 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 @@ <!-- include start from include/version/dhcp-server-version.xml.i --> -<syntaxVersion component='dhcp-server' version='7'></syntaxVersion> +<syntaxVersion component='dhcp-server' version='8'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i index ae4178c90..cb026a54a 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 @@ <!-- include start from include/version/dhcpv6-server-version.xml.i --> -<syntaxVersion component='dhcpv6-server' version='2'></syntaxVersion> +<syntaxVersion component='dhcpv6-server' version='3'></syntaxVersion> <!-- include end --> diff --git a/python/vyos/kea.py b/python/vyos/kea.py index 4a517da5f..819fe16a9 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 9f6e05ff3..bf0c09965 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -22,13 +22,10 @@ from json import loads 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 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): 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 175a67537..f163cc69a 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 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): 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 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): 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 e06a3c8a8..c1308cda7 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -18,7 +18,6 @@ import os 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 b01f510e5..f9da3d84a 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 000000000..151aa6d7b --- /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 <http://www.gnu.org/licenses/>. + +# T3316: +# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore) +# - Rename "service dhcp-server shared-network-name ... static-mapping <hostname> mac-address ..." +# to "service dhcp-server shared-network-name ... static-mapping <hostname> 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 000000000..f4bdc1d1e --- /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 <http://www.gnu.org/licenses/>. + +# 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 <hostname> identifier ..." +# to "service dhcpv6-server shared-network-name ... static-mapping <hostname> duid ..." +# - Rename "service dhcpv6-server shared-network-name ... static-mapping <hostname> mac-address ..." +# to "service dhcpv6-server shared-network-name ... static-mapping <hostname> 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) |