From c44d24eae276f77c700e961a22c0cd5582416dff Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 13 Feb 2024 00:19:30 +0100 Subject: dhcpv6-server: T5992: Fix op-mode Kea DHCP lease output Due to Kea's lease file cleanup, the CSV file content is inconsistent. This commit makes changes to use the Kea control socket to fetch current lease information. --- op-mode-definitions/dhcp.xml.in | 2 +- python/vyos/kea.py | 35 ++++++++++------------------------- src/op_mode/dhcp.py | 32 ++++++++++++++++++-------------- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in index ceb321f3e..ab305580b 100644 --- a/op-mode-definitions/dhcp.xml.in +++ b/op-mode-definitions/dhcp.xml.in @@ -130,7 +130,7 @@ Show DHCPv6 server leases sorted by the specified key - end iaid_duid ip last_communication pool remaining state type + end duid ip last_communication pool remaining state type ${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6 --sort $6 diff --git a/python/vyos/kea.py b/python/vyos/kea.py index 720bebec3..0a629fcbc 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -17,8 +17,6 @@ import json import os import socket -from datetime import datetime - from vyos.template import is_ipv6 from vyos.template import isc_static_route from vyos.template import netmask_from_cidr @@ -290,29 +288,6 @@ def kea6_parse_subnet(subnet, config): return out -def kea_parse_leases(lease_path): - contents = read_file(lease_path) - lines = contents.split("\n") - output = [] - - if len(lines) < 2: - return output - - headers = lines[0].split(",") - - for line in lines[1:]: - line_out = dict(zip(headers, line.split(","))) - - lifetime = int(line_out['valid_lifetime']) - expiry = int(line_out['expire']) - - line_out['start_timestamp'] = datetime.utcfromtimestamp(expiry - lifetime) - line_out['expire_timestamp'] = datetime.utcfromtimestamp(expiry) if expiry else None - - output.append(line_out) - - return output - def _ctrl_socket_command(path, command, args=None): if not os.path.exists(path): return None @@ -337,6 +312,16 @@ def _ctrl_socket_command(path, command, args=None): return json.loads(result.decode('utf-8')) +def kea_get_leases(inet): + ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket' + + leases = _ctrl_socket_command(ctrl_socket, f'lease{inet}-get-all') + + if not leases or 'result' not in leases or leases['result'] != 0: + return [] + + return leases['arguments']['leases'] + def kea_get_active_config(inet): ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket' diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index a64acec31..d47765414 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -29,8 +29,8 @@ from vyos.base import Warning from vyos.configquery import ConfigTreeQuery from vyos.kea import kea_get_active_config +from vyos.kea import kea_get_leases from vyos.kea import kea_get_pool_from_subnet_id -from vyos.kea import kea_parse_leases from vyos.utils.process import is_systemd_service_running time_string = "%a %b %d %H:%M:%S %Z %Y" @@ -38,7 +38,7 @@ time_string = "%a %b %d %H:%M:%S %Z %Y" config = ConfigTreeQuery() lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] sort_valid_inet = ['end', 'mac', 'hostname', 'ip', 'pool', 'remaining', 'start', 'state'] -sort_valid_inet6 = ['end', 'iaid_duid', 'ip', 'last_communication', 'pool', 'remaining', 'state', 'type'] +sort_valid_inet6 = ['end', 'duid', 'ip', 'last_communication', 'pool', 'remaining', 'state', 'type'] ArgFamily = typing.Literal['inet', 'inet6'] ArgState = typing.Literal['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] @@ -77,8 +77,7 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig :return list """ inet_suffix = '6' if family == 'inet6' else '4' - lease_file = f'/config/dhcp/dhcp{inet_suffix}-leases.csv' - leases = kea_parse_leases(lease_file) + leases = kea_get_leases(inet_suffix) if pool is None: pool = _get_dhcp_pools(family=family) @@ -89,28 +88,33 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig data = [] for lease in leases: + lifetime = lease['valid-lft'] + expiry = (lease['cltt'] + lifetime) + + lease['start_timestamp'] = datetime.utcfromtimestamp(expiry - lifetime) + lease['expire_timestamp'] = datetime.utcfromtimestamp(expiry) if expiry else None + data_lease = {} - data_lease['ip'] = lease['address'] - lease_state_long = {'0': 'active', '1': 'rejected', '2': 'expired'} + data_lease['ip'] = lease['ip-address'] + lease_state_long = {0: 'active', 1: 'rejected', 2: 'expired'} data_lease['state'] = lease_state_long[lease['state']] - data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet_id']) if active_config else '-' + data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet-id']) if active_config else '-' data_lease['end'] = lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None data_lease['origin'] = 'local' # TODO: Determine remote in HA if family == 'inet': - data_lease['mac'] = lease['hwaddr'] + data_lease['mac'] = lease['hw-address'] data_lease['start'] = lease['start_timestamp'].timestamp() data_lease['hostname'] = lease['hostname'] if family == 'inet6': data_lease['last_communication'] = lease['start_timestamp'].timestamp() - data_lease['iaid_duid'] = _format_hex_string(lease['duid']) - lease_types_long = {'0': 'non-temporary', '1': 'temporary', '2': 'prefix delegation'} - data_lease['type'] = lease_types_long[lease['lease_type']] + data_lease['duid'] = _format_hex_string(lease['duid']) + data_lease['type'] = lease['type'] data_lease['remaining'] = '-' - if lease['expire']: + if lease['valid-lft'] > 0: data_lease['remaining'] = lease['expire_timestamp'] - datetime.utcnow() if data_lease['remaining'].days >= 0: @@ -172,11 +176,11 @@ def _get_formatted_server_leases(raw_data, family='inet'): remain = lease.get('remaining') lease_type = lease.get('type') pool = lease.get('pool') - host_identifier = lease.get('iaid_duid') + host_identifier = lease.get('duid') data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier]) headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool', - 'IAID_DUID'] + 'DUID'] output = tabulate(data_entries, headers, numalign='left') return output -- cgit v1.2.3 From 6fbdab41bb3a699c3f0ab0320d7f379cf840e0b7 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 13 Feb 2024 00:50:38 +0100 Subject: dhcpv6-server: T3316: Display delegated prefix length in lease output --- src/op_mode/dhcp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index d47765414..08e46880e 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -112,6 +112,10 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig data_lease['duid'] = _format_hex_string(lease['duid']) data_lease['type'] = lease['type'] + if lease['type'] == 'IA_PD': + prefix_len = lease['prefix-len'] + data_lease['ip'] += f'/{prefix_len}' + data_lease['remaining'] = '-' if lease['valid-lft'] > 0: -- cgit v1.2.3 From dbfaa47b0036339e28ae8fd57c8007612e1b38f3 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 13 Feb 2024 01:24:27 +0100 Subject: dhcp: dhcpv6: T3316: Add op-mode for showing DHCP(v6) static-mappings --- op-mode-definitions/dhcp.xml.in | 52 +++++++++++++++++++++++++++++++++++++ src/op_mode/dhcp.py | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in index ab305580b..0db7471e5 100644 --- a/op-mode-definitions/dhcp.xml.in +++ b/op-mode-definitions/dhcp.xml.in @@ -80,6 +80,32 @@ + + + Show DHCP server static mappings + + ${vyos_op_scripts_dir}/dhcp.py show_server_static_mappings --family inet + + + + Show DHCP server static mappings for a specific pool + + service dhcp-server shared-network-name + + + ${vyos_op_scripts_dir}/dhcp.py show_server_static_mappings --family inet --pool $6 + + + + Show DHCP server static mappings sorted by the specified key + + ip mac duid pool + + + ${vyos_op_scripts_dir}/dhcp.py show_server_static_mappings --family inet --sort $6 + + + Show DHCP server statistics @@ -146,6 +172,32 @@ + + + Show DHCPv6 server static mappings + + ${vyos_op_scripts_dir}/dhcp.py show_server_static_mappings --family inet6 + + + + Show DHCPv6 server static mappings for a specific pool + + service dhcp-server shared-network-name + + + ${vyos_op_scripts_dir}/dhcp.py show_server_static_mappings --family inet6 --pool $6 + + + + Show DHCPv6 server static mappings sorted by the specified key + + ip mac duid pool + + + ${vyos_op_scripts_dir}/dhcp.py show_server_static_mappings --family inet6 --sort $6 + + + diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index 08e46880e..1d9ad0e76 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -39,6 +39,7 @@ config = ConfigTreeQuery() lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] sort_valid_inet = ['end', 'mac', 'hostname', 'ip', 'pool', 'remaining', 'start', 'state'] sort_valid_inet6 = ['end', 'duid', 'ip', 'last_communication', 'pool', 'remaining', 'state', 'type'] +mapping_sort_valid = ['mac', 'ip', 'pool', 'duid'] ArgFamily = typing.Literal['inet', 'inet6'] ArgState = typing.Literal['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] @@ -249,6 +250,47 @@ def _get_formatted_pool_statistics(pool_data, family='inet'): output = tabulate(data_entries, headers, numalign='left') return output +def _get_raw_server_static_mappings(family='inet', pool=None, sorted=None): + if pool is None: + pool = _get_dhcp_pools(family=family) + else: + pool = [pool] + + v = 'v6' if family == 'inet6' else '' + mappings = [] + for p in pool: + pool_config = config.get_config_dict(['service', f'dhcp{v}-server', 'shared-network-name', p], + get_first_key=True) + if 'subnet' in pool_config: + for subnet, subnet_config in pool_config['subnet'].items(): + if 'static-mapping' in subnet_config: + for name, mapping_config in subnet_config['static-mapping'].items(): + mapping = {'pool': p, 'subnet': subnet, 'name': name} + mapping.update(mapping_config) + mappings.append(mapping) + + if sorted: + if sorted == 'ip': + data.sort(key = lambda x:ip_address(x['ip-address'])) + else: + data.sort(key = lambda x:x[sorted]) + return mappings + +def _get_formatted_server_static_mappings(raw_data, family='inet'): + data_entries = [] + for entry in raw_data: + pool = entry.get('pool') + subnet = entry.get('subnet') + name = entry.get('name') + ip_addr = entry.get('ip-address', 'N/A') + mac_addr = entry.get('mac', 'N/A') + duid = entry.get('duid', 'N/A') + description = entry.get('description', 'N/A') + data_entries.append([pool, subnet, name, ip_addr, mac_addr, duid, description]) + + headers = ['Pool', 'Subnet', 'Name', 'IP Address', 'MAC Address', 'DUID', 'Description'] + output = tabulate(data_entries, headers, numalign='left') + return output def _verify(func): """Decorator checks if DHCP(v6) config exists""" @@ -302,6 +344,21 @@ def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str], else: return _get_formatted_server_leases(lease_data, family=family) +@_verify +def show_server_static_mappings(raw: bool, family: ArgFamily, pool: typing.Optional[str], + sorted: typing.Optional[str]): + v = 'v6' if family == 'inet6' else '' + if pool and pool not in _get_dhcp_pools(family=family): + raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!') + + if sorted and sorted not in mapping_sort_valid: + raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!') + + static_mappings = _get_raw_server_static_mappings(family=family, pool=pool, sorted=sorted) + if raw: + return static_mappings + else: + return _get_formatted_server_static_mappings(static_mappings, family=family) def _get_raw_client_leases(family='inet', interface=None): from time import mktime -- cgit v1.2.3