Skip to content

Commit

Permalink
Merge pull request #2650 from indrajitr/kea-reservation-fix
Browse files Browse the repository at this point in the history
dhcp: T3316: Support hostname, DUID and MAC address in reservation
  • Loading branch information
c-po committed Dec 28, 2023
2 parents 957c99f + 8ed44ec commit b0fc250
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 58 deletions.
20 changes: 5 additions & 15 deletions interface-definitions/dhcp-server.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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">
Expand Down
21 changes: 5 additions & 16 deletions interface-definitions/dhcpv6-server.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 -->
Original file line number Diff line number Diff line change
@@ -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 -->
29 changes: 20 additions & 9 deletions python/vyos/kea.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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)})

Expand Down Expand Up @@ -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'] ]
Expand Down Expand Up @@ -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

Expand Down
19 changes: 12 additions & 7 deletions smoketest/scripts/cli/test_service_dhcp-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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}'])
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
17 changes: 13 additions & 4 deletions smoketest/scripts/cli/test_service_dhcpv6-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions src/conf_mode/dhcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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!
Expand Down
5 changes: 5 additions & 0 deletions src/conf_mode/dhcpv6_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"!')
Expand Down
65 changes: 65 additions & 0 deletions src/migration-scripts/dhcp-server/7-to-8
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit b0fc250

Please sign in to comment.