diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/pki.py | 40 | ||||
-rwxr-xr-x | src/conf_mode/protocols_eigrp.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/protocols_rpki.py | 47 | ||||
-rwxr-xr-x | src/conf_mode/service_ipoe-server.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 10 | ||||
-rwxr-xr-x | src/conf_mode/vpn_l2tp.py | 12 | ||||
-rwxr-xr-x | src/conf_mode/vpn_pptp.py | 12 | ||||
-rwxr-xr-x | src/conf_mode/vpn_sstp.py | 88 | ||||
-rwxr-xr-x | src/init/vyos-router | 4 | ||||
-rwxr-xr-x | src/migration-scripts/ipsec/6-to-7 | 2 | ||||
-rwxr-xr-x | src/migration-scripts/l2tp/8-to-9 | 49 | ||||
-rwxr-xr-x | src/migration-scripts/rpki/1-to-2 | 22 | ||||
-rwxr-xr-x | src/op_mode/dhcp.py | 93 |
13 files changed, 309 insertions, 88 deletions
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 4be40e99e..3ab6ac5c3 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -24,11 +24,12 @@ from vyos.config import config_dict_merge from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos.configdict import node_changed -from vyos.configdiff import Diff from vyos.defaults import directories from vyos.pki import is_ca_certificate from vyos.pki import load_certificate from vyos.pki import load_public_key +from vyos.pki import load_openssh_public_key +from vyos.pki import load_openssh_private_key from vyos.pki import load_private_key from vyos.pki import load_crl from vyos.pki import load_dh_parameters @@ -64,6 +65,10 @@ sync_search = [ 'path': ['interfaces', 'sstpc'], }, { + 'keys': ['key'], + 'path': ['protocols', 'rpki', 'cache'], + }, + { 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'], 'path': ['vpn', 'ipsec'], }, @@ -86,7 +91,8 @@ sync_translate = { 'remote_key': 'key_pair', 'shared_secret_key': 'openvpn', 'auth_key': 'openvpn', - 'crypt_key': 'openvpn' + 'crypt_key': 'openvpn', + 'key': 'openssh', } def certbot_delete(certificate): @@ -150,6 +156,11 @@ def get_config(config=None): if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'key_pair' : tmp}) + tmp = node_changed(conf, base + ['openssh'], recursive=True) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'openssh' : tmp}) + tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) @@ -241,6 +252,17 @@ def is_valid_private_key(raw_data, protected=False): return True return load_private_key(raw_data, passphrase=None, wrap_tags=True) +def is_valid_openssh_public_key(raw_data, type): + # If it loads correctly we're good, or return False + return load_openssh_public_key(raw_data, type) + +def is_valid_openssh_private_key(raw_data, protected=False): + # If it loads correctly we're good, or return False + # With encrypted private keys, we always return true as we cannot ask for password to verify + if protected: + return True + return load_openssh_private_key(raw_data, passphrase=None, wrap_tags=True) + def is_valid_crl(raw_data): # If it loads correctly we're good, or return False return load_crl(raw_data, wrap_tags=True) @@ -322,6 +344,20 @@ def verify(pki): if not is_valid_private_key(private['key'], protected): raise ConfigError(f'Invalid private key on key-pair "{name}"') + if 'openssh' in pki: + for name, key_conf in pki['openssh'].items(): + if 'public' in key_conf and 'key' in key_conf['public']: + if 'type' not in key_conf['public']: + raise ConfigError(f'Must define OpenSSH public key type for "{name}"') + if not is_valid_openssh_public_key(key_conf['public']['key'], key_conf['public']['type']): + raise ConfigError(f'Invalid OpenSSH public key "{name}"') + + if 'private' in key_conf and 'key' in key_conf['private']: + private = key_conf['private'] + protected = 'password_protected' in private + if not is_valid_openssh_private_key(private['key'], protected): + raise ConfigError(f'Invalid OpenSSH private key "{name}"') + if 'x509' in pki: if 'default' in pki['x509']: default_values = pki['x509']['default'] diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py index 609b39065..c13e52a3d 100755 --- a/src/conf_mode/protocols_eigrp.py +++ b/src/conf_mode/protocols_eigrp.py @@ -19,6 +19,7 @@ from sys import argv from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_vrf from vyos.template import render_to_string from vyos import ConfigError from vyos import frr @@ -72,7 +73,14 @@ def get_config(config=None): return eigrp def verify(eigrp): - pass + if not eigrp or 'deleted' in eigrp: + return + + if 'system_as' not in eigrp: + raise ConfigError('EIGRP system-as must be defined!') + + if 'vrf' in eigrp: + verify_vrf(eigrp) def generate(eigrp): if not eigrp or 'deleted' in eigrp: diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index 0fc14e868..a59ecf3e4 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 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 @@ -16,16 +16,22 @@ import os +from glob import glob from sys import exit from vyos.config import Config +from vyos.pki import wrap_openssh_public_key +from vyos.pki import wrap_openssh_private_key from vyos.template import render_to_string -from vyos.utils.dict import dict_search +from vyos.utils.dict import dict_search_args +from vyos.utils.file import write_file from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() +rpki_ssh_key_base = '/run/frr/id_rpki' + def get_config(config=None): if config: conf = config @@ -33,7 +39,8 @@ def get_config(config=None): conf = Config() base = ['protocols', 'rpki'] - rpki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + rpki = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, with_pki=True) # Bail out early if configuration tree does not exist if not conf.exists(base): rpki.update({'deleted' : ''}) @@ -63,22 +70,40 @@ def verify(rpki): preferences.append(preference) if 'ssh' in peer_config: - files = ['private_key_file', 'public_key_file'] - for file in files: - if file not in peer_config['ssh']: - raise ConfigError('RPKI+SSH requires username and public/private ' \ - 'key file to be defined!') + if 'username' not in peer_config['ssh']: + raise ConfigError('RPKI+SSH requires username to be defined!') + + if 'key' not in peer_config['ssh'] or 'openssh' not in rpki['pki']: + raise ConfigError('RPKI+SSH requires key to be defined!') - filename = peer_config['ssh'][file] - if not os.path.exists(filename): - raise ConfigError(f'RPKI SSH {file.replace("-","-")} "{filename}" does not exist!') + if peer_config['ssh']['key'] not in rpki['pki']['openssh']: + raise ConfigError('RPKI+SSH key not found on PKI subsystem!') return None def generate(rpki): + for key in glob(f'{rpki_ssh_key_base}*'): + os.unlink(key) + if not rpki: return + + if 'cache' in rpki: + for cache, cache_config in rpki['cache'].items(): + if 'ssh' in cache_config: + key_name = cache_config['ssh']['key'] + public_key_data = dict_search_args(rpki['pki'], 'openssh', key_name, 'public', 'key') + public_key_type = dict_search_args(rpki['pki'], 'openssh', key_name, 'public', 'type') + private_key_data = dict_search_args(rpki['pki'], 'openssh', key_name, 'private', 'key') + + cache_config['ssh']['public_key_file'] = f'{rpki_ssh_key_base}_{cache}.pub' + cache_config['ssh']['private_key_file'] = f'{rpki_ssh_key_base}_{cache}' + + write_file(cache_config['ssh']['public_key_file'], wrap_openssh_public_key(public_key_data, public_key_type)) + write_file(cache_config['ssh']['private_key_file'], wrap_openssh_private_key(private_key_data)) + rpki['new_frr_config'] = render_to_string('frr/rpki.frr.j2', rpki) + return None def apply(rpki): diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 5f72b983c..852b714eb 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -25,8 +25,10 @@ from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search from vyos.accel_ppp_util import get_pools_in_order +from vyos.accel_ppp_util import verify_accel_ppp_name_servers +from vyos.accel_ppp_util import verify_accel_ppp_wins_servers from vyos.accel_ppp_util import verify_accel_ppp_ip_pool -from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos import ConfigError from vyos import airbag airbag.enable() @@ -69,8 +71,10 @@ def verify(ipoe): raise ConfigError('Option "client-subnet" incompatible with "vlan"!' 'Use "ipoe client-ip-pool" instead.') - verify_accel_ppp_base_service(ipoe, local_users=False) + verify_accel_ppp_authentication(ipoe, local_users=False) verify_accel_ppp_ip_pool(ipoe) + verify_accel_ppp_name_servers(ipoe) + verify_accel_ppp_wins_servers(ipoe) return None diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index c2dfbdb44..c9d1e805f 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -25,7 +25,9 @@ from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search -from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_name_servers +from vyos.accel_ppp_util import verify_accel_ppp_wins_servers +from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError @@ -67,11 +69,11 @@ def verify(pppoe): if not pppoe: return None - verify_accel_ppp_base_service(pppoe) + verify_accel_ppp_authentication(pppoe) verify_accel_ppp_ip_pool(pppoe) + verify_accel_ppp_name_servers(pppoe) + verify_accel_ppp_wins_servers(pppoe) - if 'wins_server' in pppoe and len(pppoe['wins_server']) > 2: - raise ConfigError('Not more then two WINS name-servers can be configured') if 'interface' not in pppoe: raise ConfigError('At least one listen interface must be defined!') diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 266381754..04ccbcec3 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -24,7 +24,9 @@ from vyos.configdict import get_accel_dict from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search -from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_name_servers +from vyos.accel_ppp_util import verify_accel_ppp_wins_servers +from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError @@ -62,12 +64,10 @@ def verify(l2tp): if not l2tp: return None - verify_accel_ppp_base_service(l2tp) + verify_accel_ppp_authentication(l2tp) verify_accel_ppp_ip_pool(l2tp) - - if 'wins_server' in l2tp and len(l2tp['wins_server']) > 2: - raise ConfigError( - 'Not more then two WINS name-servers can be configured') + verify_accel_ppp_name_servers(l2tp) + verify_accel_ppp_wins_servers(l2tp) return None diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index b1d5067d5..c0d8330bd 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -22,7 +22,9 @@ from vyos.config import Config from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search -from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_name_servers +from vyos.accel_ppp_util import verify_accel_ppp_wins_servers +from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError @@ -60,12 +62,10 @@ def verify(pptp): if not pptp: return None - verify_accel_ppp_base_service(pptp) + verify_accel_ppp_authentication(pptp) verify_accel_ppp_ip_pool(pptp) - - if 'wins_server' in pptp and len(pptp['wins_server']) > 2: - raise ConfigError( - 'Not more then two WINS name-servers can be configured') + verify_accel_ppp_name_servers(pptp) + verify_accel_ppp_wins_servers(pptp) def generate(pptp): diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 5c229fe62..8661a8aff 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -26,7 +26,9 @@ from vyos.template import render from vyos.utils.process import call from vyos.utils.network import check_port_availability from vyos.utils.dict import dict_search -from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_name_servers +from vyos.accel_ppp_util import verify_accel_ppp_wins_servers +from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import get_pools_in_order from vyos.utils.network import is_listen_port_bind_service @@ -43,48 +45,18 @@ cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem') cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key') ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem') -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['vpn', 'sstp'] - if not conf.exists(base): - return None - - # retrieve common dictionary keys - sstp = get_accel_dict(conf, base, sstp_chap_secrets, with_pki=True) - if dict_search('client_ip_pool', sstp): - # Multiple named pools require ordered values T5099 - sstp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', sstp)) - - sstp['server_type'] = 'sstp' - return sstp - - -def verify(sstp): - if not sstp: - return None - - port = sstp.get('port') - proto = 'tcp' - if check_port_availability('0.0.0.0', int(port), proto) is not True and \ - not is_listen_port_bind_service(int(port), 'accel-pppd'): - raise ConfigError(f'"{proto}" port "{port}" is used by another service') - - verify_accel_ppp_base_service(sstp) - verify_accel_ppp_ip_pool(sstp) +def verify_certificate(config): # # SSL certificate checks # - if not sstp['pki']: + if not config['pki']: raise ConfigError('PKI is not configured') - if 'ssl' not in sstp: + if 'ssl' not in config: raise ConfigError('SSL missing on SSTP config') - ssl = sstp['ssl'] + ssl = config['ssl'] # CA if 'ca_certificate' not in ssl: @@ -92,10 +64,10 @@ def verify(sstp): ca_name = ssl['ca_certificate'] - if ca_name not in sstp['pki']['ca']: + if ca_name not in config['pki']['ca']: raise ConfigError('Invalid CA certificate on SSTP config') - if 'certificate' not in sstp['pki']['ca'][ca_name]: + if 'certificate' not in config['pki']['ca'][ca_name]: raise ConfigError('Missing certificate data for CA certificate on SSTP config') # Certificate @@ -104,10 +76,10 @@ def verify(sstp): cert_name = ssl['certificate'] - if cert_name not in sstp['pki']['certificate']: + if cert_name not in config['pki']['certificate']: raise ConfigError('Invalid certificate on SSTP config') - pki_cert = sstp['pki']['certificate'][cert_name] + pki_cert = config['pki']['certificate'][cert_name] if 'certificate' not in pki_cert: raise ConfigError('Missing certificate data for certificate on SSTP config') @@ -118,6 +90,43 @@ def verify(sstp): if 'password_protected' in pki_cert['private']: raise ConfigError('Encrypted private key is not supported on SSTP config') + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['vpn', 'sstp'] + if not conf.exists(base): + return None + + # retrieve common dictionary keys + sstp = get_accel_dict(conf, base, sstp_chap_secrets, with_pki=True) + if dict_search('client_ip_pool', sstp): + # Multiple named pools require ordered values T5099 + sstp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', sstp)) + + sstp['server_type'] = 'sstp' + return sstp + + +def verify(sstp): + if not sstp: + return None + + port = sstp.get('port') + proto = 'tcp' + if check_port_availability('0.0.0.0', int(port), proto) is not True and \ + not is_listen_port_bind_service(int(port), 'accel-pppd'): + raise ConfigError(f'"{proto}" port "{port}" is used by another service') + + verify_accel_ppp_authentication(sstp) + verify_accel_ppp_ip_pool(sstp) + verify_accel_ppp_name_servers(sstp) + verify_accel_ppp_wins_servers(sstp) + verify_certificate(sstp) + + def generate(sstp): if not sstp: return None @@ -143,6 +152,7 @@ def generate(sstp): return sstp + def apply(sstp): if not sstp: call('systemctl stop accel-ppp@sstp.service') diff --git a/src/init/vyos-router b/src/init/vyos-router index 2b4fac5ef..eac3e7e47 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 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 @@ -449,7 +449,7 @@ start () run_postconfig_scripts tmp=$(${vyos_libexec_dir}/read-saved-value.py --path "protocols rpki cache") - if [ ! -z $tmp ]; then + if [[ ! -z "$tmp" ]]; then vtysh -c "rpki start" fi } diff --git a/src/migration-scripts/ipsec/6-to-7 b/src/migration-scripts/ipsec/6-to-7 index 71fbbe8a1..f8b6de560 100755 --- a/src/migration-scripts/ipsec/6-to-7 +++ b/src/migration-scripts/ipsec/6-to-7 @@ -63,7 +63,7 @@ if config.exists(ipsec_site_base): changes_made = True peer_x509_base = ipsec_site_base + [peer, 'authentication', 'x509'] - pki_name = 'peer_' + peer.replace(".", "-") + pki_name = 'peer_' + peer.replace(".", "-").replace("@", "") if config.exists(peer_x509_base + ['cert-file']): cert_file = config.return_value(peer_x509_base + ['cert-file']) diff --git a/src/migration-scripts/l2tp/8-to-9 b/src/migration-scripts/l2tp/8-to-9 new file mode 100755 index 000000000..e85a3892b --- /dev/null +++ b/src/migration-scripts/l2tp/8-to-9 @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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/>. + +# Deleted 'dhcp-interface' from l2tp + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'l2tp', 'remote-access'] +if not config.exists(base): + exit(0) + +#deleting unused dhcp-interface +if config.exists(base + ['dhcp-interface']): + config.delete(base + ['dhcp-interface']) + +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/rpki/1-to-2 b/src/migration-scripts/rpki/1-to-2 index 559440bba..50d4a3dfc 100755 --- a/src/migration-scripts/rpki/1-to-2 +++ b/src/migration-scripts/rpki/1-to-2 @@ -19,7 +19,11 @@ from sys import exit from sys import argv + from vyos.configtree import ConfigTree +from vyos.pki import OPENSSH_KEY_BEGIN +from vyos.pki import OPENSSH_KEY_END +from vyos.utils.file import read_file if len(argv) < 2: print("Must specify file name!") @@ -43,6 +47,24 @@ if config.exists(base + ['cache']): if config.exists(ssh_node + ['known-hosts-file']): config.delete(ssh_node + ['known-hosts-file']) + if config.exists(base + ['cache', cache, 'ssh']): + private_key_node = base + ['cache', cache, 'ssh', 'private-key-file'] + private_key_file = config.return_value(private_key_node) + private_key = read_file(private_key_file).replace(OPENSSH_KEY_BEGIN, '').replace(OPENSSH_KEY_END, '').replace('\n','') + + public_key_node = base + ['cache', cache, 'ssh', 'public-key-file'] + public_key_file = config.return_value(public_key_node) + public_key = read_file(public_key_file).split() + + config.set(['pki', 'openssh', f'rpki-{cache}', 'private', 'key'], value=private_key) + config.set(['pki', 'openssh', f'rpki-{cache}', 'public', 'key'], value=public_key[1]) + config.set(['pki', 'openssh', f'rpki-{cache}', 'public', 'type'], value=public_key[0]) + config.set_tag(['pki', 'openssh']) + config.set(ssh_node + ['key'], value=f'rpki-{cache}') + + config.delete(private_key_node) + config.delete(public_key_node) + try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index a64acec31..1d9ad0e76 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,8 @@ 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'] +mapping_sort_valid = ['mac', 'ip', 'pool', 'duid'] ArgFamily = typing.Literal['inet', 'inet6'] ArgState = typing.Literal['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] @@ -77,8 +78,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 +89,37 @@ 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'] + + if lease['type'] == 'IA_PD': + prefix_len = lease['prefix-len'] + data_lease['ip'] += f'/{prefix_len}' 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 +181,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 @@ -241,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""" @@ -294,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 |