From 4e1b826eecdf611fc318b2b95a27d289799ae5f4 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 17 Dec 2023 15:21:30 -0600 Subject: dhcp: T3316: Support hostname, DUID and MAC address in reservation Reinstate support for hostname in DHCP reservation. Having `hostname` in allows for server-side assignment of hostname. This is useful for static lookup of hostname. Ensure that hostname is a valid FQDN (doesn't have underscore, etc.) Additionally, support using either of DUID or MAC address for reservation. While MAC address is typically used for IPv4, and DUID is typically used for IPv6, either of them can be used in IPv4 and IPv6 reservations in Kea. --- interface-definitions/dhcp-server.xml.in | 20 +++++--------------- interface-definitions/dhcpv6-server.xml.in | 21 +++++---------------- python/vyos/kea.py | 29 ++++++++++++++++++++--------- src/conf_mode/dhcp_server.py | 10 +++++----- src/conf_mode/dhcpv6_server.py | 5 +++++ 5 files changed, 40 insertions(+), 45 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 @@ - 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 b37f79434..10fdbf3f7 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/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/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 958e90014..abccdb6a9 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 @@ -141,7 +140,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 @@ -226,9 +225,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}"!') -- cgit v1.2.3 From 94d245949d103f8fb3384ee14bb2a0e2f69c14db Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 20 Dec 2023 19:31:54 -0600 Subject: dhcp: T3316: Apply migration for valid hostname, and identifier (DUID) --- .../include/version/dhcp-server-version.xml.i | 2 +- .../include/version/dhcpv6-server-version.xml.i | 2 +- src/migration-scripts/dhcp-server/7-to-8 | 65 ++++++++++++++++++ src/migration-scripts/dhcpv6-server/2-to-3 | 78 ++++++++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) create mode 100755 src/migration-scripts/dhcp-server/7-to-8 create mode 100755 src/migration-scripts/dhcpv6-server/2-to-3 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 @@ - + 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 @@ - + 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 . + +# 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 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 . + +# 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) -- cgit v1.2.3 From 8ed44ecef860601ff82e4c5102fd11e88fe040c5 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 17 Dec 2023 21:52:53 -0600 Subject: dhcp: T3316: Update smoketest for hostname, DUID, MAC in reservation Also, have the smoketest start with a clean CLI instance. --- smoketest/scripts/cli/test_service_dhcp-server.py | 19 ++++++++++++------- smoketest/scripts/cli/test_service_dhcpv6-server.py | 17 +++++++++++++---- 2 files changed, 25 insertions(+), 11 deletions(-) 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 -- cgit v1.2.3