Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dhcp: T3316: Support hostname, DUID and MAC address in reservation #2650

Merged
merged 3 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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

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