Skip to content
Closed
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
3 changes: 3 additions & 0 deletions data/templates/dhcp-server/kea-dhcp4.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"persist": true,
"name": "{{ lease_file }}"
},
{% if client_class is vyos_defined %}
"client-classes": {{ client_class | kea_client_class_json }},
{% endif %}
"option-def": [
{
"name": "wpad-url",
Expand Down
53 changes: 53 additions & 0 deletions interface-definitions/service_dhcp-server.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,43 @@
<priority>911</priority>
</properties>
<children>
<tagNode name="client-class">
<properties>
<help>Client class name</help>
<constraint>
#include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Shared network name may only contain letters, numbers and, dots, underscores, and hyphens</constraintErrorMessage>
</properties>
<children>
#include <include/generic-disable-node.xml.i>
<node name="option82">
<properties>
<help>Match DHCP option 82 (relay agent information)</help>
</properties>
<children>
<leafNode name="circuit-id">
<properties>
<help>Filters on the contents of the circuit-id sub option.</help>
<valueHelp>
<format>txt</format>
<description>Assumes ASCII text unless input starts with 0x in which case it is interpreted as raw hex</description>
</valueHelp>
</properties>
</leafNode>
<leafNode name="remote-id">
<properties>
<help>Filters on the contents of the remote-id sub option.</help>
<valueHelp>
<format>txt</format>
<description>Assumes ASCII text unless input starts with 0x in which case it is interpreted as raw hex</description>
</valueHelp>
</properties>
</leafNode>
</children>
</node>
</children>
</tagNode>
#include <include/generic-disable-node.xml.i>
<node name="dynamic-dns-update">
<properties>
Expand Down Expand Up @@ -239,6 +276,14 @@
#include <include/dhcp/ping-check.xml.i>
#include <include/generic-description.xml.i>
#include <include/generic-disable-node.xml.i>
<leafNode name="client-class">
<properties>
<help>DHCP client class</help>
<completionHelp>
<path>service dhcp-server client-class</path>
</completionHelp>
</properties>
</leafNode>
<node name="dynamic-dns-update">
<properties>
<help>Dynamically update Domain Name System (RFC4702)</help>
Expand Down Expand Up @@ -290,6 +335,14 @@
</properties>
<children>
#include <include/dhcp/option-v4.xml.i>
<leafNode name="client-class">
<properties>
<help>DHCP client class</help>
<completionHelp>
<path>service dhcp-server client-class</path>
</completionHelp>
</properties>
</leafNode>
<leafNode name="start">
<properties>
<help>First IP address for DHCP lease range</help>
Expand Down
28 changes: 28 additions & 0 deletions python/vyos/kea.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ def kea_parse_subnet(subnet, config):
if 'ping_check' in config:
out['user-context']['enable-ping-check'] = True

if 'client_class' in config:
out['client-class'] = config['client_class']

if 'range' in config:
pools = []
for num, range_config in config['range'].items():
Expand All @@ -184,6 +187,9 @@ def kea_parse_subnet(subnet, config):
if 'bootfile_server' in range_config['option']:
pool['next-server'] = range_config['option']['bootfile_server']

if 'client_class' in range_config:
pool['client-class'] = range_config['client_class']

pools.append(pool)
out['pools'] = pools

Expand Down Expand Up @@ -661,3 +667,25 @@ def kea_get_server_leases(config, inet, pools=[], state=[], origin=None) -> list
data.pop(idx)

return data

def kea_build_client_class_test(config):
conditions = []

if "option82" in config:
if "circuit_id" in config["option82"]:
conditions.append("relay4[1].hex == 0x" + config["option82"]["circuit_id"].encode().hex().lower())
if "remote_id" in config["option82"]:
conditions.append("relay4[2].hex == 0x" + config["option82"]["remote_id"].encode().hex().lower())

is_first = True

test = ""
for condition in conditions:
if not is_first:
test += " and "

is_first = False

test += condition

return test
19 changes: 19 additions & 0 deletions python/vyos/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,25 @@ def kea_high_availability_json(config):

return dumps(data)

@register_filter('kea_client_class_json')
def kea_client_class_json(client_classes):
from vyos.kea import kea_build_client_class_test
from json import dumps
out = []

for name, config in client_classes.items():
if 'disable' in config:
continue

client_class = {
'name': name,
'test': kea_build_client_class_test(config)
}

out.append(client_class)

return dumps(out, indent=4)

@register_filter('kea_dynamic_dns_update_main_json')
def kea_dynamic_dns_update_main_json(config):
from vyos.kea import kea_parse_ddns_settings
Expand Down
100 changes: 79 additions & 21 deletions smoketest/scripts/cli/test_service_dhcp-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,27 +113,7 @@ def test_dhcp_single_pool_range(self):
range_1_start = inc_ip(subnet, 40)
range_1_stop = inc_ip(subnet, 50)

self.cli_set(base_path + ['listen-interface', interface])

self.cli_set(base_path + ['shared-network-name', shared_net_name, 'ping-check'])

pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
self.cli_set(pool + ['ignore-client-id'])
self.cli_set(pool + ['ping-check'])
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['option', 'default-router', router])
self.cli_set(pool + ['option', 'name-server', dns_1])
self.cli_set(pool + ['option', 'name-server', dns_2])
self.cli_set(pool + ['option', 'domain-name', domain_name])

# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
self.cli_set(pool + ['range', '1', 'start', range_1_start])
self.cli_set(pool + ['range', '1', 'stop', range_1_stop])
self.setup_single_pool_range(range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name)

# commit changes
self.cli_commit()
Expand Down Expand Up @@ -210,6 +190,84 @@ def test_dhcp_single_pool_range(self):
# Check for running process
self.verify_service_running()

def setup_single_pool_range(self, range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name):
self.cli_set(base_path + ['listen-interface', interface])
self.cli_set(base_path + ['shared-network-name', shared_net_name, 'ping-check'])

pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]

self.cli_set(pool + ['subnet-id', '1'])
self.cli_set(pool + ['ignore-client-id'])
self.cli_set(pool + ['ping-check'])
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['option', 'default-router', router])
self.cli_set(pool + ['option', 'name-server', dns_1])
self.cli_set(pool + ['option', 'name-server', dns_2])
self.cli_set(pool + ['option', 'domain-name', domain_name])

# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
self.cli_commit()

self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
self.cli_set(pool + ['range', '1', 'start', range_1_start])
self.cli_set(pool + ['range', '1', 'stop', range_1_stop])

def test_dhcp_client_class(self):
shared_net_name = 'SMOKE-1'

range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 40)
range_1_stop = inc_ip(subnet, 50)

self.setup_single_pool_range(range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name)

self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'client-class', 'test'])

# check validate() - Client class referenced that doesn't exist yet
with self.assertRaises(ConfigSessionError):
self.cli_commit()

self.cli_delete(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'client-class', 'test'])

self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'range', '0', 'client-class', 'test'])

# check validate() - Client class referenced that doesn't exist yet
with self.assertRaises(ConfigSessionError):
self.cli_commit()

self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'client-class', 'test'])

client_class = base_path + ['client-class', 'test']
self.cli_set(client_class + ['option82', 'circuit-id', 'foo'])
self.cli_set(client_class + ['option82', 'remote-id', 'bar'])

self.cli_commit()

config = read_file(KEA4_CONF)
obj = loads(config)

self.verify_config_value(
obj, ['Dhcp4', 'client-classes', 0], 'name', 'test'
)

self.verify_config_value(
obj, ['Dhcp4', 'client-classes', 0], 'test', 'relay4[1].hex == 0x666f6f and relay4[2].hex == 0x626172'
)

self.verify_config_value(
obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0], 'client-class', 'test'
)

self.verify_config_value(
obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0], 'client-class', 'test'
)

# Check for running process
self.verify_service_running()

def test_dhcp_single_pool_options(self):
shared_net_name = 'SMOKE-0815'

Expand Down
18 changes: 18 additions & 0 deletions src/conf_mode/service_dhcp-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,15 @@ def verify(dhcp):
f'DHCP static-route "{route}" requires router to be defined!'
)

# If a client class has been specified then it must exist
if 'client_class' in subnet_config:
client_class = subnet_config['client_class']
if 'client_class' not in dhcp:
raise ConfigError(f'Client class "{client_class}" set in subnet "{subnet}" but does not exist')

if client_class not in dhcp['client_class'].keys():
raise ConfigError(f'Client class "{client_class}" set in subnet "{subnet}" but does not exist')

# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
networks = []
Expand All @@ -240,6 +249,15 @@ def verify(dhcp):
f'DHCP range "{range}" start and stop address must be defined!'
)

# If a client class has been specified then it must exist
if 'client_class' in range_config:
client_class = range_config['client_class']
if 'client_class' not in dhcp:
raise ConfigError(f'Client class "{client_class}" set in range "{range}" but does not exist')

if client_class not in dhcp['client_class'].keys():
raise ConfigError(f'Client class "{client_class}" set in range "{range}" but does not exist')

# Start/Stop address must be inside network
for key in ['start', 'stop']:
if ip_address(range_config[key]) not in ip_network(subnet):
Expand Down
Loading