diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/accel_ppp_util.py | 4 | ||||
-rw-r--r-- | python/vyos/base.py | 21 | ||||
-rw-r--r-- | python/vyos/config.py | 11 | ||||
-rw-r--r-- | python/vyos/configdict.py | 12 | ||||
-rw-r--r-- | python/vyos/configsession.py | 55 | ||||
-rw-r--r-- | python/vyos/configsource.py | 107 | ||||
-rw-r--r-- | python/vyos/configtree.py | 1 | ||||
-rw-r--r-- | python/vyos/defaults.py | 8 | ||||
-rwxr-xr-x | python/vyos/firewall.py | 21 | ||||
-rw-r--r-- | python/vyos/frrender.py | 3 | ||||
-rw-r--r-- | python/vyos/ifconfig/wireguard.py | 160 | ||||
-rw-r--r-- | python/vyos/proto/vycall_pb2.py | 29 | ||||
-rw-r--r-- | python/vyos/proto/vyconf_pb2.py | 93 | ||||
-rw-r--r-- | python/vyos/proto/vyconf_proto.py | 377 | ||||
-rwxr-xr-x | python/vyos/template.py | 48 | ||||
-rw-r--r-- | python/vyos/utils/backend.py | 88 | ||||
-rw-r--r-- | python/vyos/utils/commit.py | 53 | ||||
-rw-r--r-- | python/vyos/utils/cpu.py | 6 | ||||
-rw-r--r-- | python/vyos/utils/network.py | 91 | ||||
-rw-r--r-- | python/vyos/utils/process.py | 48 | ||||
-rw-r--r-- | python/vyos/utils/session.py | 25 | ||||
-rw-r--r-- | python/vyos/vyconf_session.py | 119 |
22 files changed, 1202 insertions, 178 deletions
diff --git a/python/vyos/accel_ppp_util.py b/python/vyos/accel_ppp_util.py index ae75e6654..49c0e3ede 100644 --- a/python/vyos/accel_ppp_util.py +++ b/python/vyos/accel_ppp_util.py @@ -221,10 +221,12 @@ def verify_accel_ppp_ip_pool(vpn_config): for interface, interface_config in vpn_config['interface'].items(): if dict_search('client_subnet', interface_config): break + if dict_search('external_dhcp.dhcp_relay', interface_config): + break else: raise ConfigError( 'Local auth and noauth mode requires local client-ip-pool \ - or client-ipv6-pool or client-subnet to be configured!') + or client-ipv6-pool or client-subnet or dhcp-relay to be configured!') else: raise ConfigError( "Local auth mode requires local client-ip-pool \ diff --git a/python/vyos/base.py b/python/vyos/base.py index ca96d96ce..3173ddc20 100644 --- a/python/vyos/base.py +++ b/python/vyos/base.py @@ -1,4 +1,4 @@ -# Copyright 2018-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2018-2025 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,8 +15,7 @@ from textwrap import fill - -class BaseWarning: +class UserMessage: def __init__(self, header, message, **kwargs): self.message = message self.kwargs = kwargs @@ -33,7 +32,6 @@ class BaseWarning: messages = self.message.split('\n') isfirstmessage = True initial_indent = self.textinitindent - print('') for mes in messages: mes = fill(mes, initial_indent=initial_indent, subsequent_indent=self.standardindent, **self.kwargs) @@ -44,17 +42,24 @@ class BaseWarning: print('', flush=True) +class Message(): + def __init__(self, message, **kwargs): + self.Message = UserMessage('', message, **kwargs) + self.Message.print() + class Warning(): def __init__(self, message, **kwargs): - self.BaseWarn = BaseWarning('WARNING: ', message, **kwargs) - self.BaseWarn.print() + print('') + self.UserMessage = UserMessage('WARNING: ', message, **kwargs) + self.UserMessage.print() class DeprecationWarning(): def __init__(self, message, **kwargs): # Reformat the message and trim it to 72 characters in length - self.BaseWarn = BaseWarning('DEPRECATION WARNING: ', message, **kwargs) - self.BaseWarn.print() + print('') + self.UserMessage = UserMessage('DEPRECATION WARNING: ', message, **kwargs) + self.UserMessage.print() class ConfigError(Exception): diff --git a/python/vyos/config.py b/python/vyos/config.py index 546eeceab..9ae0467d4 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -1,4 +1,4 @@ -# Copyright 2017-2024 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2017-2025 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -62,6 +62,7 @@ while functions prefixed "effective" return values from the running config. In operational mode, all functions return values from the running config. """ +import os import re import json from typing import Union @@ -73,8 +74,11 @@ from vyos.xml_ref import ext_dict_merge from vyos.xml_ref import relative_defaults from vyos.utils.dict import get_sub_dict from vyos.utils.dict import mangle_dict_keys +from vyos.utils.boot import boot_configuration_complete +from vyos.utils.backend import vyconf_backend from vyos.configsource import ConfigSource from vyos.configsource import ConfigSourceSession +from vyos.configsource import ConfigSourceVyconfSession class ConfigDict(dict): _from_defaults = {} @@ -132,7 +136,10 @@ class Config(object): """ def __init__(self, session_env=None, config_source=None): if config_source is None: - self._config_source = ConfigSourceSession(session_env) + if vyconf_backend() and boot_configuration_complete(): + self._config_source = ConfigSourceVyconfSession(session_env) + else: + self._config_source = ConfigSourceSession(session_env) else: if not isinstance(config_source, ConfigSource): raise TypeError("config_source not of type ConfigSource") diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index ff0a15933..a34b0176a 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -661,6 +661,7 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False): Return a dictionary with the necessary interface config keys. """ from vyos.utils.cpu import get_core_count + from vyos.utils.cpu import get_half_cpus from vyos.template import is_ipv4 dict = config.get_config_dict(base, key_mangling=('-', '_'), @@ -670,7 +671,16 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False): with_pki=with_pki) # set CPUs cores to process requests - dict.update({'thread_count' : get_core_count()}) + match dict.get('thread_count'): + case 'all': + dict['thread_count'] = get_core_count() + case 'half': + dict['thread_count'] = get_half_cpus() + case str(x) if x.isdigit(): + dict['thread_count'] = int(x) + case _: + dict['thread_count'] = get_core_count() + # we need to store the path to the secrets file dict.update({'chap_secrets_file' : chap_secrets}) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index a3be29881..1b19c68b4 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2024 VyOS maintainers and contributors +# Copyright (C) 2019-2025 VyOS maintainers and contributors # # This library is free software; you can redistribute it and/or modify it under the terms of # the GNU Lesser General Public License as published by the Free Software Foundation; @@ -22,9 +22,10 @@ from vyos.defaults import directories from vyos.utils.process import is_systemd_service_running from vyos.utils.dict import dict_to_paths from vyos.utils.boot import boot_configuration_complete +from vyos.utils.backend import vyconf_backend from vyos.vyconf_session import VyconfSession +from vyos.base import Warning as Warn -vyconf_backend = False CLI_SHELL_API = '/bin/cli-shell-api' SET = '/opt/vyatta/sbin/my_set' @@ -120,6 +121,10 @@ def inject_vyos_env(env): env['vyos_sbin_dir'] = '/usr/sbin' env['vyos_validators_dir'] = '/usr/libexec/vyos/validators' + # with the retirement of the Cstore backend, this will remain as the + # sole indication of legacy CLI config mode, as checked by VyconfSession + env['_OFR_CONFIGURE'] = 'ok' + # if running the vyos-configd daemon, inject the vyshim env var if is_systemd_service_running('vyos-configd.service'): env['vyshim'] = '/usr/sbin/vyshim' @@ -164,37 +169,47 @@ class ConfigSession(object): for k, v in env_list: session_env[k] = v + session_env['CONFIGSESSION_PID'] = str(session_id) + self.__session_env = session_env self.__session_env['COMMIT_VIA'] = app self.__run_command([CLI_SHELL_API, 'setupSession']) - if vyconf_backend and boot_configuration_complete(): - self._vyconf_session = VyconfSession(on_error=ConfigSessionError) + if vyconf_backend() and boot_configuration_complete(): + self._vyconf_session = VyconfSession(pid=session_id, + on_error=ConfigSessionError) else: self._vyconf_session = None def __del__(self): - try: - output = ( - subprocess.check_output( - [CLI_SHELL_API, 'teardownSession'], env=self.__session_env + if self._vyconf_session is None: + try: + output = ( + subprocess.check_output( + [CLI_SHELL_API, 'teardownSession'], env=self.__session_env + ) + .decode() + .strip() ) - .decode() - .strip() - ) - if output: + if output: + print( + 'cli-shell-api teardownSession output for sesion {0}: {1}'.format( + self.__session_id, output + ), + file=sys.stderr, + ) + except Exception as e: print( - 'cli-shell-api teardownSession output for sesion {0}: {1}'.format( - self.__session_id, output - ), + 'Could not tear down session {0}: {1}'.format(self.__session_id, e), file=sys.stderr, ) - except Exception as e: - print( - 'Could not tear down session {0}: {1}'.format(self.__session_id, e), - file=sys.stderr, - ) + else: + if self._vyconf_session.session_changed(): + Warn('Exiting with uncommitted changes') + self._vyconf_session.discard() + self._vyconf_session.exit_config_mode() + self._vyconf_session.teardown() def __run_command(self, cmd_list): p = subprocess.Popen( diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py index 65cef5333..e4ced6305 100644 --- a/python/vyos/configsource.py +++ b/python/vyos/configsource.py @@ -1,5 +1,5 @@ -# Copyright 2020-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2025 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -17,9 +17,16 @@ import os import re import subprocess +from typing import Union from vyos.configtree import ConfigTree from vyos.utils.boot import boot_configuration_complete +from vyos.vyconf_session import VyconfSession +from vyos.vyconf_session import VyconfSessionError +from vyos.defaults import directories +from vyos.xml_ref import is_tag +from vyos.xml_ref import is_leaf +from vyos.xml_ref import is_multi class VyOSError(Exception): """ @@ -310,6 +317,104 @@ class ConfigSourceSession(ConfigSource): except VyOSError: return False +class ConfigSourceVyconfSession(ConfigSource): + def __init__(self, session_env=None): + super().__init__() + + if session_env: + self.__session_env = session_env + else: + self.__session_env = None + + if session_env and 'CONFIGSESSION_PID' in session_env: + self.pid = int(session_env['CONFIGSESSION_PID']) + else: + self.pid = os.getppid() + + self._vyconf_session = VyconfSession(pid=self.pid) + try: + out = self._vyconf_session.get_config() + except VyconfSessionError as e: + raise ConfigSourceError(f'Init error in {type(self)}: {e}') + + session_dir = directories['vyconf_session_dir'] + + self.running_cache_path = os.path.join(session_dir, f'running_cache_{out}') + self.session_cache_path = os.path.join(session_dir, f'session_cache_{out}') + + self._running_config = ConfigTree(internal=self.running_cache_path) + self._session_config = ConfigTree(internal=self.session_cache_path) + + # N.B. level not yet implemented pending integration with legacy CLI + # cf. T7374 + self._level = [] + + def get_level(self): + return self._level + + def set_level(self): + pass + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + try: + return self._vyconf_session.session_changed() + except VyconfSessionError: + # no actionable session info on error + return False + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + return self._vyconf_session.in_session() + + def show_config(self, path: Union[str,list] = None, default: str = None, + effective: bool = False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + + if path is None: + path = [] + if isinstance(path, str): + path = path.split() + + ct = self._running_config if effective else self._session_config + with_node = True if self.is_tag(path) else False + ct_at_path = ct.get_subtree(path, with_node=with_node) if path else ct + + res = ct_at_path.to_string().strip() + + return res if res else default + + def is_tag(self, path): + try: + return is_tag(path) + except ValueError: + return False + + def is_leaf(self, path): + try: + return is_leaf(path) + except ValueError: + return False + + def is_multi(self, path): + try: + return is_multi(path) + except ValueError: + return False + class ConfigSourceString(ConfigSource): def __init__(self, running_config_text=None, session_config_text=None): super().__init__() diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index ff40fbad0..faf124480 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -194,6 +194,7 @@ class ConfigTree(object): raise ValueError('Failed to read internal rep: {0}'.format(msg)) else: self.__config = config + self.__version = '' elif config_string is not None: config_section, version_section = extract_version(config_string) config_section = escape_backslash(config_section) diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 7efccded6..fbde0298b 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -39,14 +39,20 @@ directories = { 'completion_dir' : f'{base_dir}/completion', 'ca_certificates' : '/usr/local/share/ca-certificates/vyos', 'ppp_nexthop_dir' : '/run/ppp_nexthop', - 'proto_path' : '/usr/share/vyos/vyconf' + 'proto_path' : '/usr/share/vyos/vyconf', + 'vyconf_session_dir' : f'{base_dir}/vyconf/session' } systemd_services = { + 'haproxy' : 'haproxy.service', 'syslog' : 'syslog.service', 'snmpd' : 'snmpd.service', } +internal_ports = { + 'certbot_haproxy' : 65080, # Certbot running behing haproxy +} + config_status = '/tmp/vyos-config-status' api_config_state = '/run/http-api-state' frr_debug_enable = '/tmp/vyos.frr.debug' diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 9c320c82d..64022db84 100755 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -319,7 +319,10 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): if group_name[0] == '!': operator = '!=' group_name = group_name[1:] - output.append(f'{ip_name} {prefix}addr {operator} @R_{group_name}') + if ip_name == 'ip': + output.append(f'{ip_name} {prefix}addr {operator} @R_{group_name}') + elif ip_name == 'ip6': + output.append(f'{ip_name} {prefix}addr {operator} @R6_{group_name}') if 'mac_group' in group: group_name = group['mac_group'] operator = '' @@ -471,14 +474,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): output.append('gre version 1') if gre_key: - # The offset of the key within the packet shifts depending on the C-flag. - # nftables cannot handle complex enough expressions to match multiple + # The offset of the key within the packet shifts depending on the C-flag. + # nftables cannot handle complex enough expressions to match multiple # offsets based on bitfields elsewhere. - # We enforce a specific match for the checksum flag in validation, so the - # gre_flags dict will always have a 'checksum' key when gre_key is populated. - if not gre_flags['checksum']: + # We enforce a specific match for the checksum flag in validation, so the + # gre_flags dict will always have a 'checksum' key when gre_key is populated. + if not gre_flags['checksum']: # No "unset" child node means C is set, we offset key lookup +32 bits - output.append(f'@th,64,32 == {gre_key}') + output.append(f'@th,64,32 == {gre_key}') else: output.append(f'@th,32,32 == {gre_key}') @@ -637,7 +640,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): return " ".join(output) def parse_gre_flags(flags, force_keyed=False): - flag_map = { # nft does not have symbolic names for these. + flag_map = { # nft does not have symbolic names for these. 'checksum': 1<<0, 'routing': 1<<1, 'key': 1<<2, @@ -648,7 +651,7 @@ def parse_gre_flags(flags, force_keyed=False): include = 0 exclude = 0 for fl_name, fl_state in flags.items(): - if not fl_state: + if not fl_state: include |= flag_map[fl_name] else: # 'unset' child tag exclude |= flag_map[fl_name] diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index 524167d8b..73d6dd5f0 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -697,6 +697,9 @@ class FRRender: debug('FRR: START CONFIGURATION RENDERING') # we can not reload an empty file, thus we always embed the marker output = '!\n' + # Enable FRR logging + output += 'log syslog\n' + output += 'log facility local7\n' # Enable SNMP agentx support # SNMP AgentX support cannot be disabled once enabled if 'snmp' in config_dict: diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index f5217aecb..3a28723b3 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -22,12 +22,13 @@ from tempfile import NamedTemporaryFile from hurry.filesize import size from hurry.filesize import alternative +from vyos.base import Warning from vyos.configquery import ConfigTreeQuery from vyos.ifconfig import Interface from vyos.ifconfig import Operational from vyos.template import is_ipv6 from vyos.template import is_ipv4 - +from vyos.utils.network import get_wireguard_peers class WireGuardOperational(Operational): def _dump(self): """Dump wireguard data in a python friendly way.""" @@ -251,92 +252,131 @@ class WireGuardIf(Interface): """Get a synthetic MAC address.""" return self.get_mac_synthetic() + def get_peer_public_keys(self, config, disabled=False): + """Get list of configured peer public keys""" + if 'peer' not in config: + return [] + + public_keys = [] + + for _, peer_config in config['peer'].items(): + if disabled == ('disable' in peer_config): + public_keys.append(peer_config['public_key']) + + return public_keys + def update(self, config): """General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered interface setup code and provide a single point of entry when workin on any interface.""" - tmp_file = NamedTemporaryFile('w') - tmp_file.write(config['private_key']) - tmp_file.flush() # Wireguard base command is identical for every peer base_cmd = f'wg set {self.ifname}' + interface_cmd = base_cmd if 'port' in config: interface_cmd += ' listen-port {port}' if 'fwmark' in config: interface_cmd += ' fwmark {fwmark}' - interface_cmd += f' private-key {tmp_file.name}' - interface_cmd = interface_cmd.format(**config) - # T6490: execute command to ensure interface configured - self._cmd(interface_cmd) + with NamedTemporaryFile('w') as tmp_file: + tmp_file.write(config['private_key']) + tmp_file.flush() - # If no PSK is given remove it by using /dev/null - passing keys via - # the shell (usually bash) is considered insecure, thus we use a file - no_psk_file = '/dev/null' + interface_cmd += f' private-key {tmp_file.name}' + interface_cmd = interface_cmd.format(**config) + # T6490: execute command to ensure interface configured + self._cmd(interface_cmd) + + current_peer_public_keys = get_wireguard_peers(self.ifname) + + if 'rebuild_required' in config: + # Remove all existing peers that no longer exist in config + current_public_keys = self.get_peer_public_keys(config) + cmd_remove_peers = [f' peer {public_key} remove' + for public_key in current_peer_public_keys + if public_key not in current_public_keys] + if cmd_remove_peers: + self._cmd(base_cmd + ''.join(cmd_remove_peers)) if 'peer' in config: + # Group removal of disabled peers in one command + current_disabled_peers = self.get_peer_public_keys(config, disabled=True) + cmd_disabled_peers = [f' peer {public_key} remove' + for public_key in current_disabled_peers] + if cmd_disabled_peers: + self._cmd(base_cmd + ''.join(cmd_disabled_peers)) + + peer_cmds = [] + peer_domain_cmds = [] + peer_psk_files = [] + for peer, peer_config in config['peer'].items(): # T4702: No need to configure this peer when it was explicitly # marked as disabled - also active sessions are terminated as # the public key was already removed when entering this method! if 'disable' in peer_config: - # remove peer if disabled, no error report even if peer not exists - cmd = base_cmd + ' peer {public_key} remove' - self._cmd(cmd.format(**peer_config)) continue - psk_file = no_psk_file - # start of with a fresh 'wg' command - peer_cmd = base_cmd + ' peer {public_key}' + peer_cmd = ' peer {public_key}' - try: - cmd = peer_cmd - - if 'preshared_key' in peer_config: - psk_file = '/tmp/tmp.wireguard.psk' - with open(psk_file, 'w') as f: - f.write(peer_config['preshared_key']) - cmd += f' preshared-key {psk_file}' - - # Persistent keepalive is optional - if 'persistent_keepalive' in peer_config: - cmd += ' persistent-keepalive {persistent_keepalive}' - - # Multiple allowed-ip ranges can be defined - ensure we are always - # dealing with a list - if isinstance(peer_config['allowed_ips'], str): - peer_config['allowed_ips'] = [peer_config['allowed_ips']] - cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips']) - - self._cmd(cmd.format(**peer_config)) - - cmd = peer_cmd - - # Ensure peer is created even if dns not working - if {'address', 'port'} <= set(peer_config): - if is_ipv6(peer_config['address']): - cmd += ' endpoint [{address}]:{port}' - elif is_ipv4(peer_config['address']): - cmd += ' endpoint {address}:{port}' - else: - # don't set endpoint if address uses domain name - continue - elif {'host_name', 'port'} <= set(peer_config): - cmd += ' endpoint {host_name}:{port}' - - self._cmd(cmd.format(**peer_config), env={ + cmd = peer_cmd + + if 'preshared_key' in peer_config: + with NamedTemporaryFile(mode='w', delete=False) as tmp_file: + tmp_file.write(peer_config['preshared_key']) + tmp_file.flush() + cmd += f' preshared-key {tmp_file.name}' + peer_psk_files.append(tmp_file.name) + else: + # If no PSK is given remove it by using /dev/null - passing keys via + # the shell (usually bash) is considered insecure, thus we use a file + cmd += f' preshared-key /dev/null' + + # Persistent keepalive is optional + if 'persistent_keepalive' in peer_config: + cmd += ' persistent-keepalive {persistent_keepalive}' + + # Multiple allowed-ip ranges can be defined - ensure we are always + # dealing with a list + if isinstance(peer_config['allowed_ips'], str): + peer_config['allowed_ips'] = [peer_config['allowed_ips']] + cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips']) + + peer_cmds.append(cmd.format(**peer_config)) + + cmd = peer_cmd + + # Ensure peer is created even if dns not working + if {'address', 'port'} <= set(peer_config): + if is_ipv6(peer_config['address']): + cmd += ' endpoint [{address}]:{port}' + elif is_ipv4(peer_config['address']): + cmd += ' endpoint {address}:{port}' + else: + # don't set endpoint if address uses domain name + continue + elif {'host_name', 'port'} <= set(peer_config): + cmd += ' endpoint {host_name}:{port}' + else: + continue + + peer_domain_cmds.append(cmd.format(**peer_config)) + + try: + if peer_cmds: + self._cmd(base_cmd + ''.join(peer_cmds)) + + if peer_domain_cmds: + self._cmd(base_cmd + ''.join(peer_domain_cmds), env={ 'WG_ENDPOINT_RESOLUTION_RETRIES': config['max_dns_retry']}) - except: - # todo: logging - pass - finally: - # PSK key file is not required to be stored persistently as its backed by CLI - if psk_file != no_psk_file and os.path.exists(psk_file): - os.remove(psk_file) + except Exception as e: + Warning(f'Failed to apply Wireguard peers on {self.ifname}: {e}') + finally: + for tmp in peer_psk_files: + os.unlink(tmp) # call base class super().update(config) diff --git a/python/vyos/proto/vycall_pb2.py b/python/vyos/proto/vycall_pb2.py new file mode 100644 index 000000000..95214d2a6 --- /dev/null +++ b/python/vyos/proto/vycall_pb2.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: vycall.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cvycall.proto\"&\n\x06Status\x12\x0f\n\x07success\x18\x01 \x02(\x08\x12\x0b\n\x03out\x18\x02 \x02(\t\"Y\n\x04\x43\x61ll\x12\x13\n\x0bscript_name\x18\x01 \x02(\t\x12\x11\n\ttag_value\x18\x02 \x01(\t\x12\x11\n\targ_value\x18\x03 \x01(\t\x12\x16\n\x05reply\x18\x04 \x01(\x0b\x32\x07.Status\"~\n\x06\x43ommit\x12\x12\n\nsession_id\x18\x01 \x02(\t\x12\x0f\n\x07\x64ry_run\x18\x04 \x02(\x08\x12\x0e\n\x06\x61tomic\x18\x05 \x02(\x08\x12\x12\n\nbackground\x18\x06 \x02(\x08\x12\x15\n\x04init\x18\x07 \x01(\x0b\x32\x07.Status\x12\x14\n\x05\x63\x61lls\x18\x08 \x03(\x0b\x32\x05.Call') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'vycall_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _STATUS._serialized_start=16 + _STATUS._serialized_end=54 + _CALL._serialized_start=56 + _CALL._serialized_end=145 + _COMMIT._serialized_start=147 + _COMMIT._serialized_end=273 +# @@protoc_insertion_point(module_scope) diff --git a/python/vyos/proto/vyconf_pb2.py b/python/vyos/proto/vyconf_pb2.py new file mode 100644 index 000000000..3d5042888 --- /dev/null +++ b/python/vyos/proto/vyconf_pb2.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: vyconf.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cvyconf.proto\"\x89\x15\n\x07Request\x12!\n\x06prompt\x18\x01 \x01(\x0b\x32\x0f.Request.PromptH\x00\x12.\n\rsetup_session\x18\x02 \x01(\x0b\x32\x15.Request.SetupSessionH\x00\x12\x1b\n\x03set\x18\x03 \x01(\x0b\x32\x0c.Request.SetH\x00\x12!\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x0f.Request.DeleteH\x00\x12!\n\x06rename\x18\x05 \x01(\x0b\x32\x0f.Request.RenameH\x00\x12\x1d\n\x04\x63opy\x18\x06 \x01(\x0b\x32\r.Request.CopyH\x00\x12#\n\x07\x63omment\x18\x07 \x01(\x0b\x32\x10.Request.CommentH\x00\x12!\n\x06\x63ommit\x18\x08 \x01(\x0b\x32\x0f.Request.CommitH\x00\x12%\n\x08rollback\x18\t \x01(\x0b\x32\x11.Request.RollbackH\x00\x12\x1f\n\x05merge\x18\n \x01(\x0b\x32\x0e.Request.MergeH\x00\x12\x1d\n\x04save\x18\x0b \x01(\x0b\x32\r.Request.SaveH\x00\x12*\n\x0bshow_config\x18\x0c \x01(\x0b\x32\x13.Request.ShowConfigH\x00\x12!\n\x06\x65xists\x18\r \x01(\x0b\x32\x0f.Request.ExistsH\x00\x12&\n\tget_value\x18\x0e \x01(\x0b\x32\x11.Request.GetValueH\x00\x12(\n\nget_values\x18\x0f \x01(\x0b\x32\x12.Request.GetValuesH\x00\x12.\n\rlist_children\x18\x10 \x01(\x0b\x32\x15.Request.ListChildrenH\x00\x12)\n\x0brun_op_mode\x18\x11 \x01(\x0b\x32\x12.Request.RunOpModeH\x00\x12#\n\x07\x63onfirm\x18\x12 \x01(\x0b\x32\x10.Request.ConfirmH\x00\x12\x43\n\x18\x65nter_configuration_mode\x18\x13 \x01(\x0b\x32\x1f.Request.EnterConfigurationModeH\x00\x12\x41\n\x17\x65xit_configuration_mode\x18\x14 \x01(\x0b\x32\x1e.Request.ExitConfigurationModeH\x00\x12%\n\x08validate\x18\x15 \x01(\x0b\x32\x11.Request.ValidateH\x00\x12%\n\x08teardown\x18\x16 \x01(\x0b\x32\x11.Request.TeardownH\x00\x12\x30\n\x0ereload_reftree\x18\x17 \x01(\x0b\x32\x16.Request.ReloadReftreeH\x00\x12\x1d\n\x04load\x18\x18 \x01(\x0b\x32\r.Request.LoadH\x00\x12#\n\x07\x64iscard\x18\x19 \x01(\x0b\x32\x10.Request.DiscardH\x00\x12\x32\n\x0fsession_changed\x18\x1a \x01(\x0b\x32\x17.Request.SessionChangedH\x00\x12/\n\x0esession_of_pid\x18\x1b \x01(\x0b\x32\x15.Request.SessionOfPidH\x00\x12\x37\n\x12session_update_pid\x18\x1c \x01(\x0b\x32\x19.Request.SessionUpdatePidH\x00\x12(\n\nget_config\x18\x1d \x01(\x0b\x32\x12.Request.GetConfigH\x00\x1a\x08\n\x06Prompt\x1aP\n\x0cSetupSession\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x12\x19\n\x11\x43lientApplication\x18\x02 \x01(\t\x12\x12\n\nOnBehalfOf\x18\x03 \x01(\x05\x1a!\n\x0cSessionOfPid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a%\n\x10SessionUpdatePid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a\x1a\n\tGetConfig\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1e\n\x08Teardown\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\x1a\x46\n\x08Validate\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\x13\n\x03Set\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x16\n\x06\x44\x65lete\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x18\n\x07\x44iscard\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1f\n\x0eSessionChanged\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x35\n\x06Rename\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a\x33\n\x04\x43opy\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a(\n\x07\x43omment\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12\x0f\n\x07\x43omment\x18\x02 \x02(\t\x1aR\n\x06\x43ommit\x12\x0f\n\x07\x43onfirm\x18\x01 \x01(\x08\x12\x16\n\x0e\x43onfirmTimeout\x18\x02 \x01(\x05\x12\x0f\n\x07\x43omment\x18\x03 \x01(\t\x12\x0e\n\x06\x44ryRun\x18\x04 \x01(\x08\x1a\x1c\n\x08Rollback\x12\x10\n\x08Revision\x18\x01 \x02(\x05\x1a?\n\x04Load\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a@\n\x05Merge\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a?\n\x04Save\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x41\n\nShowConfig\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x16\n\x06\x45xists\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x46\n\x08GetValue\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tGetValues\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aJ\n\x0cListChildren\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tRunOpMode\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\t\n\x07\x43onfirm\x1a\x46\n\x16\x45nterConfigurationMode\x12\x11\n\tExclusive\x18\x01 \x02(\x08\x12\x19\n\x11OverrideExclusive\x18\x02 \x02(\x08\x1a\x17\n\x15\x45xitConfigurationMode\x1a#\n\rReloadReftree\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\"#\n\x0c\x43onfigFormat\x12\t\n\x05\x43URLY\x10\x00\x12\x08\n\x04JSON\x10\x01\")\n\x0cOutputFormat\x12\x0c\n\x08OutPlain\x10\x00\x12\x0b\n\x07OutJSON\x10\x01\x42\x05\n\x03msg\";\n\x0fRequestEnvelope\x12\r\n\x05token\x18\x01 \x01(\t\x12\x19\n\x07request\x18\x02 \x02(\x0b\x32\x08.Request\"S\n\x08Response\x12\x17\n\x06status\x18\x01 \x02(\x0e\x32\x07.Errnum\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\x0f\n\x07warning\x18\x04 \x01(\t*\xd2\x01\n\x06\x45rrnum\x12\x0b\n\x07SUCCESS\x10\x00\x12\x08\n\x04\x46\x41IL\x10\x01\x12\x10\n\x0cINVALID_PATH\x10\x02\x12\x11\n\rINVALID_VALUE\x10\x03\x12\x16\n\x12\x43OMMIT_IN_PROGRESS\x10\x04\x12\x18\n\x14\x43ONFIGURATION_LOCKED\x10\x05\x12\x12\n\x0eINTERNAL_ERROR\x10\x06\x12\x15\n\x11PERMISSION_DENIED\x10\x07\x12\x17\n\x13PATH_ALREADY_EXISTS\x10\x08\x12\x16\n\x12UNCOMMITED_CHANGES\x10\t') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'vyconf_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _ERRNUM._serialized_start=2863 + _ERRNUM._serialized_end=3073 + _REQUEST._serialized_start=17 + _REQUEST._serialized_end=2714 + _REQUEST_PROMPT._serialized_start=1237 + _REQUEST_PROMPT._serialized_end=1245 + _REQUEST_SETUPSESSION._serialized_start=1247 + _REQUEST_SETUPSESSION._serialized_end=1327 + _REQUEST_SESSIONOFPID._serialized_start=1329 + _REQUEST_SESSIONOFPID._serialized_end=1362 + _REQUEST_SESSIONUPDATEPID._serialized_start=1364 + _REQUEST_SESSIONUPDATEPID._serialized_end=1401 + _REQUEST_GETCONFIG._serialized_start=1403 + _REQUEST_GETCONFIG._serialized_end=1429 + _REQUEST_TEARDOWN._serialized_start=1431 + _REQUEST_TEARDOWN._serialized_end=1461 + _REQUEST_VALIDATE._serialized_start=1463 + _REQUEST_VALIDATE._serialized_end=1533 + _REQUEST_SET._serialized_start=1535 + _REQUEST_SET._serialized_end=1554 + _REQUEST_DELETE._serialized_start=1556 + _REQUEST_DELETE._serialized_end=1578 + _REQUEST_DISCARD._serialized_start=1580 + _REQUEST_DISCARD._serialized_end=1604 + _REQUEST_SESSIONCHANGED._serialized_start=1606 + _REQUEST_SESSIONCHANGED._serialized_end=1637 + _REQUEST_RENAME._serialized_start=1639 + _REQUEST_RENAME._serialized_end=1692 + _REQUEST_COPY._serialized_start=1694 + _REQUEST_COPY._serialized_end=1745 + _REQUEST_COMMENT._serialized_start=1747 + _REQUEST_COMMENT._serialized_end=1787 + _REQUEST_COMMIT._serialized_start=1789 + _REQUEST_COMMIT._serialized_end=1871 + _REQUEST_ROLLBACK._serialized_start=1873 + _REQUEST_ROLLBACK._serialized_end=1901 + _REQUEST_LOAD._serialized_start=1903 + _REQUEST_LOAD._serialized_end=1966 + _REQUEST_MERGE._serialized_start=1968 + _REQUEST_MERGE._serialized_end=2032 + _REQUEST_SAVE._serialized_start=2034 + _REQUEST_SAVE._serialized_end=2097 + _REQUEST_SHOWCONFIG._serialized_start=2099 + _REQUEST_SHOWCONFIG._serialized_end=2164 + _REQUEST_EXISTS._serialized_start=2166 + _REQUEST_EXISTS._serialized_end=2188 + _REQUEST_GETVALUE._serialized_start=2190 + _REQUEST_GETVALUE._serialized_end=2260 + _REQUEST_GETVALUES._serialized_start=2262 + _REQUEST_GETVALUES._serialized_end=2333 + _REQUEST_LISTCHILDREN._serialized_start=2335 + _REQUEST_LISTCHILDREN._serialized_end=2409 + _REQUEST_RUNOPMODE._serialized_start=2411 + _REQUEST_RUNOPMODE._serialized_end=2482 + _REQUEST_CONFIRM._serialized_start=1799 + _REQUEST_CONFIRM._serialized_end=1808 + _REQUEST_ENTERCONFIGURATIONMODE._serialized_start=2495 + _REQUEST_ENTERCONFIGURATIONMODE._serialized_end=2565 + _REQUEST_EXITCONFIGURATIONMODE._serialized_start=2567 + _REQUEST_EXITCONFIGURATIONMODE._serialized_end=2590 + _REQUEST_RELOADREFTREE._serialized_start=2592 + _REQUEST_RELOADREFTREE._serialized_end=2627 + _REQUEST_CONFIGFORMAT._serialized_start=2629 + _REQUEST_CONFIGFORMAT._serialized_end=2664 + _REQUEST_OUTPUTFORMAT._serialized_start=2666 + _REQUEST_OUTPUTFORMAT._serialized_end=2707 + _REQUESTENVELOPE._serialized_start=2716 + _REQUESTENVELOPE._serialized_end=2775 + _RESPONSE._serialized_start=2777 + _RESPONSE._serialized_end=2860 +# @@protoc_insertion_point(module_scope) diff --git a/python/vyos/proto/vyconf_proto.py b/python/vyos/proto/vyconf_proto.py new file mode 100644 index 000000000..404ef2f27 --- /dev/null +++ b/python/vyos/proto/vyconf_proto.py @@ -0,0 +1,377 @@ +from enum import IntEnum +from dataclasses import dataclass +from dataclasses import field + +class Errnum(IntEnum): + SUCCESS = 0 + FAIL = 1 + INVALID_PATH = 2 + INVALID_VALUE = 3 + COMMIT_IN_PROGRESS = 4 + CONFIGURATION_LOCKED = 5 + INTERNAL_ERROR = 6 + PERMISSION_DENIED = 7 + PATH_ALREADY_EXISTS = 8 + UNCOMMITED_CHANGES = 9 + +class ConfigFormat(IntEnum): + CURLY = 0 + JSON = 1 + +class OutputFormat(IntEnum): + OutPlain = 0 + OutJSON = 1 + +@dataclass +class Prompt: + pass + +@dataclass +class SetupSession: + ClientPid: int = 0 + ClientApplication: str = None + OnBehalfOf: int = None + +@dataclass +class SessionOfPid: + ClientPid: int = 0 + +@dataclass +class SessionUpdatePid: + ClientPid: int = 0 + +@dataclass +class GetConfig: + dummy: int = None + +@dataclass +class Teardown: + OnBehalfOf: int = None + +@dataclass +class Validate: + Path: list[str] = field(default_factory=list) + output_format: OutputFormat = None + +@dataclass +class Set: + Path: list[str] = field(default_factory=list) + +@dataclass +class Delete: + Path: list[str] = field(default_factory=list) + +@dataclass +class Discard: + dummy: int = None + +@dataclass +class SessionChanged: + dummy: int = None + +@dataclass +class Rename: + EditLevel: list[str] = field(default_factory=list) + From: str = "" + To: str = "" + +@dataclass +class Copy: + EditLevel: list[str] = field(default_factory=list) + From: str = "" + To: str = "" + +@dataclass +class Comment: + Path: list[str] = field(default_factory=list) + Comment: str = "" + +@dataclass +class Commit: + Confirm: bool = None + ConfirmTimeout: int = None + Comment: str = None + DryRun: bool = None + +@dataclass +class Rollback: + Revision: int = 0 + +@dataclass +class Load: + Location: str = "" + format: ConfigFormat = None + +@dataclass +class Merge: + Location: str = "" + format: ConfigFormat = None + +@dataclass +class Save: + Location: str = "" + format: ConfigFormat = None + +@dataclass +class ShowConfig: + Path: list[str] = field(default_factory=list) + format: ConfigFormat = None + +@dataclass +class Exists: + Path: list[str] = field(default_factory=list) + +@dataclass +class GetValue: + Path: list[str] = field(default_factory=list) + output_format: OutputFormat = None + +@dataclass +class GetValues: + Path: list[str] = field(default_factory=list) + output_format: OutputFormat = None + +@dataclass +class ListChildren: + Path: list[str] = field(default_factory=list) + output_format: OutputFormat = None + +@dataclass +class RunOpMode: + Path: list[str] = field(default_factory=list) + output_format: OutputFormat = None + +@dataclass +class Confirm: + pass + +@dataclass +class EnterConfigurationMode: + Exclusive: bool = False + OverrideExclusive: bool = False + +@dataclass +class ExitConfigurationMode: + pass + +@dataclass +class ReloadReftree: + OnBehalfOf: int = None + +@dataclass +class Request: + prompt: Prompt = None + setup_session: SetupSession = None + set: Set = None + delete: Delete = None + rename: Rename = None + copy: Copy = None + comment: Comment = None + commit: Commit = None + rollback: Rollback = None + merge: Merge = None + save: Save = None + show_config: ShowConfig = None + exists: Exists = None + get_value: GetValue = None + get_values: GetValues = None + list_children: ListChildren = None + run_op_mode: RunOpMode = None + confirm: Confirm = None + enter_configuration_mode: EnterConfigurationMode = None + exit_configuration_mode: ExitConfigurationMode = None + validate: Validate = None + teardown: Teardown = None + reload_reftree: ReloadReftree = None + load: Load = None + discard: Discard = None + session_changed: SessionChanged = None + session_of_pid: SessionOfPid = None + session_update_pid: SessionUpdatePid = None + get_config: GetConfig = None + +@dataclass +class RequestEnvelope: + token: str = None + request: Request = None + +@dataclass +class Response: + status: Errnum = None + output: str = None + error: str = None + warning: str = None + +def set_request_prompt(token: str = None): + reqi = Prompt () + req = Request(prompt=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_setup_session(token: str = None, client_pid: int = 0, client_application: str = None, on_behalf_of: int = None): + reqi = SetupSession (client_pid, client_application, on_behalf_of) + req = Request(setup_session=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_session_of_pid(token: str = None, client_pid: int = 0): + reqi = SessionOfPid (client_pid) + req = Request(session_of_pid=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_session_update_pid(token: str = None, client_pid: int = 0): + reqi = SessionUpdatePid (client_pid) + req = Request(session_update_pid=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_get_config(token: str = None, dummy: int = None): + reqi = GetConfig (dummy) + req = Request(get_config=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_teardown(token: str = None, on_behalf_of: int = None): + reqi = Teardown (on_behalf_of) + req = Request(teardown=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_validate(token: str = None, path: list[str] = [], output_format: OutputFormat = None): + reqi = Validate (path, output_format) + req = Request(validate=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_set(token: str = None, path: list[str] = []): + reqi = Set (path) + req = Request(set=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_delete(token: str = None, path: list[str] = []): + reqi = Delete (path) + req = Request(delete=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_discard(token: str = None, dummy: int = None): + reqi = Discard (dummy) + req = Request(discard=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_session_changed(token: str = None, dummy: int = None): + reqi = SessionChanged (dummy) + req = Request(session_changed=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_rename(token: str = None, edit_level: list[str] = [], from_: str = "", to: str = ""): + reqi = Rename (edit_level, from_, to) + req = Request(rename=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_copy(token: str = None, edit_level: list[str] = [], from_: str = "", to: str = ""): + reqi = Copy (edit_level, from_, to) + req = Request(copy=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_comment(token: str = None, path: list[str] = [], comment: str = ""): + reqi = Comment (path, comment) + req = Request(comment=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_commit(token: str = None, confirm: bool = None, confirm_timeout: int = None, comment: str = None, dry_run: bool = None): + reqi = Commit (confirm, confirm_timeout, comment, dry_run) + req = Request(commit=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_rollback(token: str = None, revision: int = 0): + reqi = Rollback (revision) + req = Request(rollback=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_load(token: str = None, location: str = "", format: ConfigFormat = None): + reqi = Load (location, format) + req = Request(load=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_merge(token: str = None, location: str = "", format: ConfigFormat = None): + reqi = Merge (location, format) + req = Request(merge=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_save(token: str = None, location: str = "", format: ConfigFormat = None): + reqi = Save (location, format) + req = Request(save=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_show_config(token: str = None, path: list[str] = [], format: ConfigFormat = None): + reqi = ShowConfig (path, format) + req = Request(show_config=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_exists(token: str = None, path: list[str] = []): + reqi = Exists (path) + req = Request(exists=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_get_value(token: str = None, path: list[str] = [], output_format: OutputFormat = None): + reqi = GetValue (path, output_format) + req = Request(get_value=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_get_values(token: str = None, path: list[str] = [], output_format: OutputFormat = None): + reqi = GetValues (path, output_format) + req = Request(get_values=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_list_children(token: str = None, path: list[str] = [], output_format: OutputFormat = None): + reqi = ListChildren (path, output_format) + req = Request(list_children=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_run_op_mode(token: str = None, path: list[str] = [], output_format: OutputFormat = None): + reqi = RunOpMode (path, output_format) + req = Request(run_op_mode=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_confirm(token: str = None): + reqi = Confirm () + req = Request(confirm=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_enter_configuration_mode(token: str = None, exclusive: bool = False, override_exclusive: bool = False): + reqi = EnterConfigurationMode (exclusive, override_exclusive) + req = Request(enter_configuration_mode=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_exit_configuration_mode(token: str = None): + reqi = ExitConfigurationMode () + req = Request(exit_configuration_mode=reqi) + req_env = RequestEnvelope(token, req) + return req_env + +def set_request_reload_reftree(token: str = None, on_behalf_of: int = None): + reqi = ReloadReftree (on_behalf_of) + req = Request(reload_reftree=reqi) + req_env = RequestEnvelope(token, req) + return req_env diff --git a/python/vyos/template.py b/python/vyos/template.py index d79e1183f..aa215db95 100755 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -36,6 +36,7 @@ DEFAULT_TEMPLATE_DIR = directories["templates"] # Holds template filters registered via register_filter() _FILTERS = {} _TESTS = {} +_CLEVER_FUNCTIONS = {} # reuse Environments with identical settings to improve performance @functools.lru_cache(maxsize=2) @@ -58,6 +59,7 @@ def _get_environment(location=None): ) env.filters.update(_FILTERS) env.tests.update(_TESTS) + env.globals.update(_CLEVER_FUNCTIONS) return env @@ -77,7 +79,7 @@ def register_filter(name, func=None): "Filters can only be registered before rendering the first template" ) if name in _FILTERS: - raise ValueError(f"A filter with name {name!r} was registered already") + raise ValueError(f"A filter with name {name!r} was already registered") _FILTERS[name] = func return func @@ -97,10 +99,30 @@ def register_test(name, func=None): "Tests can only be registered before rendering the first template" ) if name in _TESTS: - raise ValueError(f"A test with name {name!r} was registered already") + raise ValueError(f"A test with name {name!r} was already registered") _TESTS[name] = func return func +def register_clever_function(name, func=None): + """Register a function to be available as test in templates under given name. + + It can also be used as a decorator, see below in this module for examples. + + :raise RuntimeError: + when trying to register a test after a template has been rendered already + :raise ValueError: when trying to register a name which was taken already + """ + if func is None: + return functools.partial(register_clever_function, name) + if _get_environment.cache_info().currsize: + raise RuntimeError( + "Clever functions can only be registered before rendering the" \ + "first template") + if name in _CLEVER_FUNCTIONS: + raise ValueError(f"A clever function with name {name!r} was already "\ + "registered") + _CLEVER_FUNCTIONS[name] = func + return func def render_to_string(template, content, formater=None, location=None): """Render a template from the template directory, raise on any errors. @@ -150,6 +172,8 @@ def render( # As we are opening the file with 'w', we are performing the rendering before # calling open() to not accidentally erase the file if rendering fails rendered = render_to_string(template, content, formater, location) + # Remove any trailing character and always add a new line at the end + rendered = rendered.rstrip() + "\n" # Write to file with open(destination, "w") as file: @@ -704,7 +728,7 @@ def conntrack_rule(rule_conf, rule_id, action, ipv6=False): if port[0] == '!': operator = '!=' port = port[1:] - output.append(f'th {prefix}port {operator} {port}') + output.append(f'th {prefix}port {operator} {{ {port} }}') if 'group' in side_conf: group = side_conf['group'] @@ -1050,3 +1074,21 @@ def vyos_defined(value, test_value=None, var_type=None): else: # Valid value and is matching optional argument if provided - return true return True + +@register_clever_function('get_default_port') +def get_default_port(service): + """ + Jinja2 plugin to retrieve common service port number from vyos.defaults + class form a Jinja2 template. This removes the need to hardcode, or pass in + the data using the general dictionary. + + Added to remove code complexity and make it easier to read. + + Example: + {{ get_default_port('certbot_haproxy') }} + """ + from vyos.defaults import internal_ports + if service not in internal_ports: + raise RuntimeError(f'Service "{service}" not found in internal ' \ + 'vyos.defaults.internal_ports dict!') + return internal_ports[service] diff --git a/python/vyos/utils/backend.py b/python/vyos/utils/backend.py new file mode 100644 index 000000000..400ea9b69 --- /dev/null +++ b/python/vyos/utils/backend.py @@ -0,0 +1,88 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# N.B. the following is a temporary addition for running smoketests under +# vyconf and is not to be called explicitly, at the risk of catastophe. + +# pylint: disable=wrong-import-position + +from pathlib import Path + +from vyos.utils.io import ask_yes_no +from vyos.utils.process import call + +VYCONF_SENTINEL = '/run/vyconf_backend' + +MSG_ENABLE_VYCONF = 'This will enable the vyconf backend for testing. Proceed?' +MSG_DISABLE_VYCONF = ( + 'This will restore the legacy backend; it requires a reboot. Proceed?' +) + +# read/set immutable file attribute without popen: +# https://www.geeklab.info/2021/04/chattr-and-lsattr-in-python/ +import fcntl # pylint: disable=C0411 # noqa: E402 +from array import array # pylint: disable=C0411 # noqa: E402 + +# FS constants - see /uapi/linux/fs.h in kernel source +# or <elixir.free-electrons.com/linux/latest/source/include/uapi/linux/fs.h> +FS_IOC_GETFLAGS = 0x80086601 +FS_IOC_SETFLAGS = 0x40086602 +FS_IMMUTABLE_FL = 0x010 + + +def chattri(filename: str, value: bool): + with open(filename, 'r') as f: + arg = array('L', [0]) + fcntl.ioctl(f.fileno(), FS_IOC_GETFLAGS, arg, True) + if value: + arg[0] = arg[0] | FS_IMMUTABLE_FL + else: + arg[0] = arg[0] & ~FS_IMMUTABLE_FL + fcntl.ioctl(f.fileno(), FS_IOC_SETFLAGS, arg, True) + + +def lsattri(filename: str) -> bool: + with open(filename, 'r') as f: + arg = array('L', [0]) + fcntl.ioctl(f.fileno(), FS_IOC_GETFLAGS, arg, True) + return bool(arg[0] & FS_IMMUTABLE_FL) + + +# End: read/set immutable file attribute without popen + + +def vyconf_backend() -> bool: + return Path(VYCONF_SENTINEL).exists() and lsattri(VYCONF_SENTINEL) + + +def set_vyconf_backend(value: bool, no_prompt: bool = False): + vyconfd_service = 'vyconfd.service' + match value: + case True: + if vyconf_backend(): + return + if not no_prompt and not ask_yes_no(MSG_ENABLE_VYCONF): + return + Path(VYCONF_SENTINEL).touch() + chattri(VYCONF_SENTINEL, True) + call(f'systemctl restart {vyconfd_service}') + case False: + if not vyconf_backend(): + return + if not no_prompt and not ask_yes_no(MSG_DISABLE_VYCONF): + return + chattri(VYCONF_SENTINEL, False) + Path(VYCONF_SENTINEL).unlink() + call('/sbin/shutdown -r now') diff --git a/python/vyos/utils/commit.py b/python/vyos/utils/commit.py index 105aed8c2..9167c78d2 100644 --- a/python/vyos/utils/commit.py +++ b/python/vyos/utils/commit.py @@ -1,4 +1,4 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2025 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -13,8 +13,13 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. +# pylint: disable=import-outside-toplevel + +from typing import IO + + def commit_in_progress(): - """ Not to be used in normal op mode scripts! """ + """Not to be used in normal op mode scripts!""" # The CStore backend locks the config by opening a file # The file is not removed after commit, so just checking @@ -36,7 +41,9 @@ def commit_in_progress(): from vyos.defaults import commit_lock if getuser() != 'root': - raise OSError('This functions needs to be run as root to return correct results!') + raise OSError( + 'This functions needs to be run as root to return correct results!' + ) for proc in process_iter(): try: @@ -45,7 +52,7 @@ def commit_in_progress(): for f in files: if f.path == commit_lock: return True - except NoSuchProcess as err: + except NoSuchProcess: # Process died before we could examine it pass # Default case @@ -53,8 +60,44 @@ def commit_in_progress(): def wait_for_commit_lock(): - """ Not to be used in normal op mode scripts! """ + """Not to be used in normal op mode scripts!""" from time import sleep + # Very synchronous approach to multiprocessing while commit_in_progress(): sleep(1) + + +# For transitional compatibility with the legacy commit locking mechanism, +# we require a lockf/fcntl (POSIX-type) lock, hence the following in place +# of vyos.utils.locking + + +def acquire_commit_lock_file() -> tuple[IO, str]: + import fcntl + from pathlib import Path + from vyos.defaults import commit_lock + + try: + # pylint: disable=consider-using-with + lock_fd = Path(commit_lock).open('w') + except IOError as e: + out = f'Critical error opening commit lock file {e}' + return None, out + + try: + fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + return lock_fd, '' + except IOError: + out = 'Configuration system locked by another commit in progress' + lock_fd.close() + return None, out + + +def release_commit_lock_file(file_descr): + import fcntl + + if file_descr is None: + return + fcntl.lockf(file_descr, fcntl.LOCK_UN) + file_descr.close() diff --git a/python/vyos/utils/cpu.py b/python/vyos/utils/cpu.py index 8ace77d15..6f21eb526 100644 --- a/python/vyos/utils/cpu.py +++ b/python/vyos/utils/cpu.py @@ -26,6 +26,7 @@ It has special cases for x86_64 and MAY work correctly on other architectures, but nothing is certain. """ +import os import re def _read_cpuinfo(): @@ -114,3 +115,8 @@ def get_available_cpus(): out = json.loads(cmd('lscpu --extended -b --json')) return out['cpus'] + + +def get_half_cpus(): + """ return 1/2 of the numbers of available CPUs """ + return max(1, os.cpu_count() // 2) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 2f666f0ee..0a84be478 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -256,40 +256,60 @@ def mac2eui64(mac, prefix=None): except: # pylint: disable=bare-except return -def check_port_availability(ipaddress, port, protocol): +def check_port_availability(address: str=None, port: int=0, protocol: str='tcp') -> bool: """ - Check if port is available and not used by any service - Return False if a port is busy or IP address does not exists + Check if given port is available and not used by any service. + Should be used carefully for services that can start listening dynamically, because IP address may be dynamic too + + Args: + address: IPv4 or IPv6 address - if None, checks on all interfaces + port: TCP/UDP port number. + + + Returns: + False if a port is busy or IP address does not exists + True if a port is free and IP address exists """ - from socketserver import TCPServer, UDPServer + import socket from ipaddress import ip_address + # treat None as "any address" + address = address or '::' + # verify arguments try: - ipaddress = ip_address(ipaddress).compressed - except: - raise ValueError(f'The {ipaddress} is not a valid IPv4 or IPv6 address') + address = ip_address(address).compressed + except ValueError: + raise ValueError(f'{address} is not a valid IPv4 or IPv6 address') if port not in range(1, 65536): - raise ValueError(f'The port number {port} is not in the 1-65535 range') + raise ValueError(f'Port {port} is not in range 1-65535') if protocol not in ['tcp', 'udp']: - raise ValueError(f'The protocol {protocol} is not supported. Only tcp and udp are allowed') + raise ValueError(f'{protocol} is not supported - only tcp and udp are allowed') - # check port availability + protocol = socket.SOCK_STREAM if protocol == 'tcp' else socket.SOCK_DGRAM try: - if protocol == 'tcp': - server = TCPServer((ipaddress, port), None, bind_and_activate=True) - if protocol == 'udp': - server = UDPServer((ipaddress, port), None, bind_and_activate=True) - server.server_close() - except Exception as e: - # errno.h: - #define EADDRINUSE 98 /* Address already in use */ - if e.errno == 98: + addr_info = socket.getaddrinfo(address, port, socket.AF_UNSPEC, protocol) + except socket.gaierror as e: + print(f'Invalid address: {address}') + return False + + for family, socktype, proto, canonname, sockaddr in addr_info: + try: + with socket.socket(family, socktype, proto) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(sockaddr) + # port is free to use + return True + except OSError: + # port is already in use return False - return True + # if we reach this point, no socket was tested and we assume the port is + # already in use - better safe then sorry + return False + def is_listen_port_bind_service(port: int, service: str) -> bool: """Check if listen port bound to expected program name @@ -396,6 +416,21 @@ def is_wireguard_key_pair(private_key: str, public_key:str) -> bool: else: return False +def get_wireguard_peers(ifname: str) -> list: + """ + Return list of configured Wireguard peers for interface + :param ifname: Interface name + :type ifname: str + :return: list of public keys + :rtype: list + """ + if not interface_exists(ifname): + return [] + + from vyos.utils.process import cmd + peers = cmd(f'wg show {ifname} peers') + return peers.splitlines() + def is_subnet_connected(subnet, primary=False): """ Verify is the given IPv4/IPv6 subnet is connected to any interface on this @@ -615,3 +650,19 @@ def is_valid_ipv4_address_or_range(addr: str) -> bool: return ip_network(addr).version == 4 except: return False + +def is_valid_ipv6_address_or_range(addr: str) -> bool: + """ + Validates if the provided address is a valid IPv4, CIDR or IPv4 range + :param addr: address to test + :return: bool: True if provided address is valid + """ + from ipaddress import ip_network + try: + if '-' in addr: # If we are checking a range, validate both address's individually + split = addr.split('-') + return is_valid_ipv6_address_or_range(split[0]) and is_valid_ipv6_address_or_range(split[1]) + else: + return ip_network(addr).version == 6 + except: + return False diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index 121b6e240..21335e6b3 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import shlex from subprocess import Popen from subprocess import PIPE @@ -21,20 +22,17 @@ from subprocess import STDOUT from subprocess import DEVNULL -def get_wrapper(vrf, netns, auth): - wrapper = '' +def get_wrapper(vrf, netns): + wrapper = None if vrf: - wrapper = f'ip vrf exec {vrf} ' + wrapper = ['ip', 'vrf', 'exec', vrf] elif netns: - wrapper = f'ip netns exec {netns} ' - if auth: - wrapper = f'{auth} {wrapper}' + wrapper = ['ip', 'netns', 'exec', netns] return wrapper def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=PIPE, decode='utf-8', auth='', vrf=None, - netns=None): + stdout=PIPE, stderr=PIPE, decode='utf-8', vrf=None, netns=None): """ popen is a wrapper helper around subprocess.Popen with it default setting it will return a tuple (out, err) @@ -75,28 +73,33 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, if not debug.enabled(flag): flag = 'command' + use_shell = shell + stdin = None + if shell is None: + use_shell = False + if ' ' in command: + use_shell = True + if env: + use_shell = True + # Must be run as root to execute command in VRF or network namespace + wrapper = get_wrapper(vrf, netns) if vrf or netns: if os.getuid() != 0: raise OSError( 'Permission denied: cannot execute commands in VRF and netns contexts as an unprivileged user' ) - wrapper = get_wrapper(vrf, netns, auth) - command = f'{wrapper} {command}' if wrapper else command + if use_shell: + command = f'{shlex.join(wrapper)} {command}' + else: + if type(command) is not list: + command = [command] + command = wrapper + command - cmd_msg = f"cmd '{command}'" + cmd_msg = f"cmd '{command}'" if use_shell else f"cmd '{shlex.join(command)}'" debug.message(cmd_msg, flag) - use_shell = shell - stdin = None - if shell is None: - use_shell = False - if ' ' in command: - use_shell = True - if env: - use_shell = True - if input: stdin = PIPE input = input.encode() if type(input) is str else input @@ -155,7 +158,7 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None, def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, stdout=PIPE, stderr=PIPE, decode='utf-8', raising=None, message='', - expect=[0], auth='', vrf=None, netns=None): + expect=[0], vrf=None, netns=None): """ A wrapper around popen, which returns the stdout and will raise the error code of a command @@ -171,12 +174,11 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, input=input, timeout=timeout, env=env, shell=shell, decode=decode, - auth=auth, vrf=vrf, netns=netns, ) if code not in expect: - wrapper = get_wrapper(vrf, netns, auth='') + wrapper = get_wrapper(vrf, netns) command = f'{wrapper} {command}' feedback = message + '\n' if message else '' feedback += f'failed to run command: {command}\n' diff --git a/python/vyos/utils/session.py b/python/vyos/utils/session.py new file mode 100644 index 000000000..28559dc59 --- /dev/null +++ b/python/vyos/utils/session.py @@ -0,0 +1,25 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# pylint: disable=import-outside-toplevel + + +def in_config_session(): + """Vyatta bash completion uses the following environment variable for + indication of the config mode environment, independent of legacy backend + initialization of Cstore""" + from os import environ + + return '_OFR_CONFIGURE' in environ diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py index 506095625..4250f0cfb 100644 --- a/python/vyos/vyconf_session.py +++ b/python/vyos/vyconf_session.py @@ -15,6 +15,7 @@ # # +import os import tempfile import shutil from functools import wraps @@ -24,26 +25,86 @@ from vyos.proto import vyconf_client from vyos.migrate import ConfigMigrate from vyos.migrate import ConfigMigrateError from vyos.component_version import append_system_version +from vyos.utils.session import in_config_session +from vyos.proto.vyconf_proto import Errnum +from vyos.utils.commit import acquire_commit_lock_file +from vyos.utils.commit import release_commit_lock_file -def output(o): - out = '' - for res in (o.output, o.error, o.warning): - if res is not None: - out = out + res - return out +class VyconfSessionError(Exception): + pass class VyconfSession: - def __init__(self, token: str = None, on_error: Type[Exception] = None): + def __init__( + self, token: str = None, pid: int = None, on_error: Type[Exception] = None + ): + self.pid = os.getpid() if pid is None else pid if token is None: - out = vyconf_client.send_request('setup_session') + # CLI applications with arg pid=getppid() allow coordination + # with the ambient session; other uses (such as ConfigSession) + # may default to self pid + out = vyconf_client.send_request('session_of_pid', client_pid=self.pid) + if out.output is None: + out = vyconf_client.send_request('setup_session', client_pid=self.pid) self.__token = out.output else: + out = vyconf_client.send_request( + 'session_update_pid', token=token, client_pid=self.pid + ) + if out.status: + raise ValueError(f'No existing session for token: {token}') self.__token = token + self.in_config_session = in_config_session() + if self.in_config_session: + out = vyconf_client.send_request( + 'enter_configuration_mode', token=self.__token + ) + if out.status: + raise VyconfSessionError(self.output(out)) + self.on_error = on_error + def __del__(self): + if not self.in_config_session: + self.teardown() + + def teardown(self): + vyconf_client.send_request('teardown', token=self.__token) + + def exit_config_mode(self): + if self.session_changed(): + return 'Uncommited changes', Errnum.UNCOMMITED_CHANGES + out = vyconf_client.send_request('exit_configuration_mode', token=self.__token) + return self.output(out), out.status + + def in_session(self) -> bool: + return self.in_config_session + + def session_changed(self) -> bool: + out = vyconf_client.send_request('session_changed', token=self.__token) + return not bool(out.status) + + def get_config(self): + out = vyconf_client.send_request('get_config', token=self.__token) + if out.status: + raise VyconfSessionError(self.output(out)) + return out.output + + @staticmethod + def config_mode(f): + @wraps(f) + def wrapped(self, *args, **kwargs): + msg = 'operation not available outside of config mode' + if not self.in_config_session: + if self.on_error is None: + raise VyconfSessionError(msg) + raise self.on_error(msg) + return f(self, *args, **kwargs) + + return wrapped + @staticmethod def raise_exception(f): @wraps(f) @@ -57,31 +118,46 @@ class VyconfSession: return wrapped + @staticmethod + def output(o): + out = '' + for res in (o.output, o.error, o.warning): + if res is not None: + out = out + res + return out + @raise_exception + @config_mode def set(self, path: list[str]) -> tuple[str, int]: out = vyconf_client.send_request('set', token=self.__token, path=path) - return output(out), out.status + return self.output(out), out.status @raise_exception + @config_mode def delete(self, path: list[str]) -> tuple[str, int]: out = vyconf_client.send_request('delete', token=self.__token, path=path) - return output(out), out.status + return self.output(out), out.status @raise_exception + @config_mode def commit(self) -> tuple[str, int]: + lock_fd, out = acquire_commit_lock_file() + if lock_fd is None: + return out, Errnum.COMMIT_IN_PROGRESS + out = vyconf_client.send_request('commit', token=self.__token) - return output(out), out.status + release_commit_lock_file(lock_fd) + + return self.output(out), out.status @raise_exception + @config_mode def discard(self) -> tuple[str, int]: out = vyconf_client.send_request('discard', token=self.__token) - return output(out), out.status - - def session_changed(self) -> bool: - out = vyconf_client.send_request('session_changed', token=self.__token) - return not bool(out.status) + return self.output(out), out.status @raise_exception + @config_mode def load_config(self, file: str, migrate: bool = False) -> tuple[str, int]: # pylint: disable=consider-using-with if migrate: @@ -101,23 +177,18 @@ class VyconfSession: if tmp: tmp.close() - return output(out), out.status + return self.output(out), out.status @raise_exception def save_config(self, file: str, append_version: bool = False) -> tuple[str, int]: out = vyconf_client.send_request('save', token=self.__token, location=file) if append_version: append_system_version(file) - return output(out), out.status + return self.output(out), out.status @raise_exception def show_config(self, path: list[str] = None) -> tuple[str, int]: if path is None: path = [] out = vyconf_client.send_request('show_config', token=self.__token, path=path) - return output(out), out.status - - def __del__(self): - out = vyconf_client.send_request('teardown', token=self.__token) - if out.status: - print(f'Could not tear down session {self.__token}: {output(out)}') + return self.output(out), out.status |