summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2023-12-28 13:07:41 +0100
committerGitHub <noreply@github.com>2023-12-28 13:07:41 +0100
commitb0fc2507aab533c1d6c4472667f39f9c11f40e69 (patch)
treeaf1adf71a752cb3cdfd8a7f5dfe2d977ddd0bd7a
parent957c99f20f9c08b0fc5dd51664a06cae16bc52c1 (diff)
parent8ed44ecef860601ff82e4c5102fd11e88fe040c5 (diff)
downloadvyos-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.in20
-rw-r--r--interface-definitions/dhcpv6-server.xml.in21
-rw-r--r--interface-definitions/include/version/dhcp-server-version.xml.i2
-rw-r--r--interface-definitions/include/version/dhcpv6-server-version.xml.i2
-rw-r--r--python/vyos/kea.py29
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-server.py19
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcpv6-server.py17
-rwxr-xr-xsrc/conf_mode/dhcp_server.py10
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py5
-rwxr-xr-xsrc/migration-scripts/dhcp-server/7-to-865
-rwxr-xr-xsrc/migration-scripts/dhcpv6-server/2-to-378
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)