summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/component_version.py63
-rw-r--r--python/vyos/configsession.py55
-rw-r--r--python/vyos/frrender.py2
-rw-r--r--python/vyos/kea.py67
-rw-r--r--python/vyos/proto/vyconf_client.py4
-rw-r--r--python/vyos/system/grub_util.py5
-rwxr-xr-xpython/vyos/template.py98
-rw-r--r--python/vyos/vyconf_session.py123
8 files changed, 358 insertions, 59 deletions
diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py
index 94215531d..81d986658 100644
--- a/python/vyos/component_version.py
+++ b/python/vyos/component_version.py
@@ -49,7 +49,9 @@ DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot')
REGEX_WARN_VYOS = r'(// Warning: Do not remove the following line.)'
REGEX_WARN_VYATTA = r'(/\* Warning: Do not remove the following line. \*/)'
REGEX_COMPONENT_VERSION_VYOS = r'// vyos-config-version:\s+"([\w@:-]+)"\s*'
-REGEX_COMPONENT_VERSION_VYATTA = r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/'
+REGEX_COMPONENT_VERSION_VYATTA = (
+ r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/'
+)
REGEX_RELEASE_VERSION_VYOS = r'// Release version:\s+(\S*)\s*'
REGEX_RELEASE_VERSION_VYATTA = r'/\* Release version:\s+(\S*)\s*\*/'
@@ -62,16 +64,31 @@ CONFIG_FILE_VERSION = """\
warn_filter_vyos = re.compile(REGEX_WARN_VYOS)
warn_filter_vyatta = re.compile(REGEX_WARN_VYATTA)
-regex_filter = { 'vyos': dict(zip(['component', 'release'],
- [re.compile(REGEX_COMPONENT_VERSION_VYOS),
- re.compile(REGEX_RELEASE_VERSION_VYOS)])),
- 'vyatta': dict(zip(['component', 'release'],
- [re.compile(REGEX_COMPONENT_VERSION_VYATTA),
- re.compile(REGEX_RELEASE_VERSION_VYATTA)])) }
+regex_filter = {
+ 'vyos': dict(
+ zip(
+ ['component', 'release'],
+ [
+ re.compile(REGEX_COMPONENT_VERSION_VYOS),
+ re.compile(REGEX_RELEASE_VERSION_VYOS),
+ ],
+ )
+ ),
+ 'vyatta': dict(
+ zip(
+ ['component', 'release'],
+ [
+ re.compile(REGEX_COMPONENT_VERSION_VYATTA),
+ re.compile(REGEX_RELEASE_VERSION_VYATTA),
+ ],
+ )
+ ),
+}
+
@dataclass
class VersionInfo:
- component: Optional[dict[str,int]] = None
+ component: Optional[dict[str, int]] = None
release: str = get_version()
vintage: str = 'vyos'
config_body: Optional[str] = None
@@ -84,8 +101,9 @@ class VersionInfo:
return bool(self.config_body is None)
def update_footer(self):
- f = CONFIG_FILE_VERSION.format(component_to_string(self.component),
- self.release)
+ f = CONFIG_FILE_VERSION.format(
+ component_to_string(self.component), self.release
+ )
self.footer_lines = f.splitlines()
def update_syntax(self):
@@ -121,13 +139,16 @@ class VersionInfo:
except Exception as e:
raise ValueError(e) from e
+
def component_to_string(component: dict) -> str:
- l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])]
+ l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])] # noqa: E741
return ':'.join(l)
+
def component_from_string(string: str) -> dict:
return {k: int(v) for k, v in re.findall(r'([\w,-]+)@(\d+)', string)}
+
def version_info_from_file(config_file) -> VersionInfo:
"""Return config file component and release version info."""
version_info = VersionInfo()
@@ -166,27 +187,27 @@ def version_info_from_file(config_file) -> VersionInfo:
return version_info
+
def version_info_from_system() -> VersionInfo:
"""Return system component and release version info."""
d = component_version()
sort_d = dict(sorted(d.items(), key=lambda x: x[0]))
- version_info = VersionInfo(
- component = sort_d,
- release = get_version(),
- vintage = 'vyos'
- )
+ version_info = VersionInfo(component=sort_d, release=get_version(), vintage='vyos')
return version_info
+
def version_info_copy(v: VersionInfo) -> VersionInfo:
"""Make a copy of dataclass."""
return replace(v)
+
def version_info_prune_component(x: VersionInfo, y: VersionInfo) -> VersionInfo:
"""In place pruning of component keys of x not in y."""
if x.component is None or y.component is None:
return
- x.component = { k: v for k,v in x.component.items() if k in y.component }
+ x.component = {k: v for k, v in x.component.items() if k in y.component}
+
def add_system_version(config_str: str = None, out_file: str = None):
"""Wrap config string with system version and write to out_file.
@@ -202,3 +223,11 @@ def add_system_version(config_str: str = None, out_file: str = None):
version_info.write(out_file)
else:
sys.stdout.write(version_info.write_string())
+
+
+def append_system_version(file: str):
+ """Append system version data to existing file"""
+ version_info = version_info_from_system()
+ version_info.update_footer()
+ with open(file, 'a') as f:
+ f.write(version_info.write_string())
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 90b96b88c..a3be29881 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -21,6 +21,10 @@ import subprocess
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.vyconf_session import VyconfSession
+
+vyconf_backend = False
CLI_SHELL_API = '/bin/cli-shell-api'
SET = '/opt/vyatta/sbin/my_set'
@@ -165,6 +169,11 @@ class ConfigSession(object):
self.__run_command([CLI_SHELL_API, 'setupSession'])
+ if vyconf_backend and boot_configuration_complete():
+ self._vyconf_session = VyconfSession(on_error=ConfigSessionError)
+ else:
+ self._vyconf_session = None
+
def __del__(self):
try:
output = (
@@ -209,7 +218,10 @@ class ConfigSession(object):
value = []
else:
value = [value]
- self.__run_command([SET] + path + value)
+ if self._vyconf_session is None:
+ self.__run_command([SET] + path + value)
+ else:
+ self._vyconf_session.set(path + value)
def set_section(self, path: list, d: dict):
try:
@@ -223,7 +235,10 @@ class ConfigSession(object):
value = []
else:
value = [value]
- self.__run_command([DELETE] + path + value)
+ if self._vyconf_session is None:
+ self.__run_command([DELETE] + path + value)
+ else:
+ self._vyconf_session.delete(path + value)
def load_section(self, path: list, d: dict):
try:
@@ -261,20 +276,34 @@ class ConfigSession(object):
self.__run_command([COMMENT] + path + value)
def commit(self):
- out = self.__run_command([COMMIT])
+ if self._vyconf_session is None:
+ out = self.__run_command([COMMIT])
+ else:
+ out, _ = self._vyconf_session.commit()
+
return out
def discard(self):
- self.__run_command([DISCARD])
+ if self._vyconf_session is None:
+ self.__run_command([DISCARD])
+ else:
+ out, _ = self._vyconf_session.discard()
def show_config(self, path, format='raw'):
- config_data = self.__run_command(SHOW_CONFIG + path)
+ if self._vyconf_session is None:
+ config_data = self.__run_command(SHOW_CONFIG + path)
+ else:
+ config_data, _ = self._vyconf_session.show_config()
if format == 'raw':
return config_data
def load_config(self, file_path):
- out = self.__run_command(LOAD_CONFIG + [file_path])
+ if self._vyconf_session is None:
+ out = self.__run_command(LOAD_CONFIG + [file_path])
+ else:
+ out, _ = self._vyconf_session.load_config(file=file_path)
+
return out
def load_explicit(self, file_path):
@@ -287,11 +316,21 @@ class ConfigSession(object):
raise ConfigSessionError(e) from e
def migrate_and_load_config(self, file_path):
- out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path])
+ if self._vyconf_session is None:
+ out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path])
+ else:
+ out, _ = self._vyconf_session.load_config(file=file_path, migrate=True)
+
return out
def save_config(self, file_path):
- out = self.__run_command(SAVE_CONFIG + [file_path])
+ if self._vyconf_session is None:
+ out = self.__run_command(SAVE_CONFIG + [file_path])
+ else:
+ out, _ = self._vyconf_session.save_config(
+ file=file_path, append_version=True
+ )
+
return out
def install_image(self, url):
diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py
index 8d469e3e2..524167d8b 100644
--- a/python/vyos/frrender.py
+++ b/python/vyos/frrender.py
@@ -92,7 +92,7 @@ def get_frrender_dict(conf, argv=None) -> dict:
if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None:
del default_values['area'][area_num]['area_type']['nssa']
- for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
+ for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static']:
if dict_search(f'redistribute.{protocol}', ospf) is None:
del default_values['redistribute'][protocol]
if not bool(default_values['redistribute']):
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 9fc5dde3d..5eecbbaad 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -20,8 +20,8 @@ import socket
from datetime import datetime
from datetime import timezone
+from vyos import ConfigError
from vyos.template import is_ipv6
-from vyos.template import isc_static_route
from vyos.template import netmask_from_cidr
from vyos.utils.dict import dict_search_args
from vyos.utils.file import file_permissions
@@ -44,6 +44,7 @@ kea4_options = {
'wpad_url': 'wpad-url',
'ipv6_only_preferred': 'v6-only-preferred',
'captive_portal': 'v4-captive-portal',
+ 'capwap_controller': 'capwap-ac-v4',
}
kea6_options = {
@@ -56,6 +57,7 @@ kea6_options = {
'nisplus_server': 'nisp-servers',
'sntp_server': 'sntp-servers',
'captive_portal': 'v6-captive-portal',
+ 'capwap_controller': 'capwap-ac-v6',
}
kea_ctrl_socket = '/run/kea/dhcp{inet}-ctrl-socket'
@@ -111,22 +113,21 @@ def kea_parse_options(config):
default_route = ''
if 'default_router' in config:
- default_route = isc_static_route('0.0.0.0/0', config['default_router'])
+ default_route = f'0.0.0.0/0 - {config["default_router"]}'
routes = [
- isc_static_route(route, route_options['next_hop'])
+ f'{route} - {route_options["next_hop"]}'
for route, route_options in config['static_route'].items()
]
options.append(
{
- 'name': 'rfc3442-static-route',
+ 'name': 'classless-static-route',
'data': ', '.join(
routes if not default_route else routes + [default_route]
),
}
)
- options.append({'name': 'windows-static-route', 'data': ', '.join(routes)})
if 'time_zone' in config:
with open('/usr/share/zoneinfo/' + config['time_zone'], 'rb') as f:
@@ -147,7 +148,7 @@ def kea_parse_options(config):
def kea_parse_subnet(subnet, config):
- out = {'subnet': subnet, 'id': int(config['subnet_id'])}
+ out = {'subnet': subnet, 'id': int(config['subnet_id']), 'user-context': {}}
if 'option' in config:
out['option-data'] = kea_parse_options(config['option'])
@@ -165,6 +166,9 @@ def kea_parse_subnet(subnet, config):
out['valid-lifetime'] = int(config['lease'])
out['max-valid-lifetime'] = int(config['lease'])
+ if 'ping_check' in config:
+ out['user-context']['enable-ping-check'] = True
+
if 'range' in config:
pools = []
for num, range_config in config['range'].items():
@@ -218,6 +222,9 @@ def kea_parse_subnet(subnet, config):
reservations.append(reservation)
out['reservations'] = reservations
+ if 'dynamic_dns_update' in config:
+ out.update(kea_parse_ddns_settings(config['dynamic_dns_update']))
+
return out
@@ -347,6 +354,54 @@ def kea6_parse_subnet(subnet, config):
return out
+def kea_parse_tsig_algo(algo_spec):
+ translate = {
+ 'md5': 'HMAC-MD5',
+ 'sha1': 'HMAC-SHA1',
+ 'sha224': 'HMAC-SHA224',
+ 'sha256': 'HMAC-SHA256',
+ 'sha384': 'HMAC-SHA384',
+ 'sha512': 'HMAC-SHA512'
+ }
+ if algo_spec not in translate:
+ raise ConfigError(f'Unsupported TSIG algorithm: {algo_spec}')
+ return translate[algo_spec]
+
+def kea_parse_enable_disable(value):
+ return True if value == 'enable' else False
+
+def kea_parse_ddns_settings(config):
+ data = {}
+
+ if send_updates := config.get('send_updates'):
+ data['ddns-send-updates'] = kea_parse_enable_disable(send_updates)
+
+ if override_client_update := config.get('override_client_update'):
+ data['ddns-override-client-update'] = kea_parse_enable_disable(override_client_update)
+
+ if override_no_update := config.get('override_no_update'):
+ data['ddns-override-no-update'] = kea_parse_enable_disable(override_no_update)
+
+ if update_on_renew := config.get('update_on_renew'):
+ data['ddns-update-on-renew'] = kea_parse_enable_disable(update_on_renew)
+
+ if conflict_resolution := config.get('conflict_resolution'):
+ data['ddns-use-conflict-resolution'] = kea_parse_enable_disable(conflict_resolution)
+
+ if 'replace_client_name' in config:
+ data['ddns-replace-client-name'] = config['replace_client_name']
+ if 'generated_prefix' in config:
+ data['ddns-generated-prefix'] = config['generated_prefix']
+ if 'qualifying_suffix' in config:
+ data['ddns-qualifying-suffix'] = config['qualifying_suffix']
+ if 'ttl_percent' in config:
+ data['ddns-ttl-percent'] = int(config['ttl_percent']) / 100
+ if 'hostname_char_set' in config:
+ data['hostname-char-set'] = config['hostname_char_set']
+ if 'hostname_char_replacement' in config:
+ data['hostname-char-replacement'] = config['hostname_char_replacement']
+
+ return data
def _ctrl_socket_command(inet, command, args=None):
path = kea_ctrl_socket.format(inet=inet)
diff --git a/python/vyos/proto/vyconf_client.py b/python/vyos/proto/vyconf_client.py
index f34549309..b385f0951 100644
--- a/python/vyos/proto/vyconf_client.py
+++ b/python/vyos/proto/vyconf_client.py
@@ -52,7 +52,9 @@ def request_to_msg(req: vyconf_proto.RequestEnvelope) -> vyconf_pb2.RequestEnvel
def msg_to_response(msg: vyconf_pb2.Response) -> vyconf_proto.Response:
# pylint: disable=no-member
- d = MessageToDict(msg, preserving_proto_field_name=True)
+ d = MessageToDict(
+ msg, preserving_proto_field_name=True, use_integers_for_enums=True
+ )
response = vyconf_proto.Response(**d)
return response
diff --git a/python/vyos/system/grub_util.py b/python/vyos/system/grub_util.py
index 4a3d8795e..ad95bb4f9 100644
--- a/python/vyos/system/grub_util.py
+++ b/python/vyos/system/grub_util.py
@@ -56,13 +56,12 @@ def set_kernel_cmdline_options(cmdline_options: str, version: str = '',
@image.if_not_live_boot
def update_kernel_cmdline_options(cmdline_options: str,
- root_dir: str = '') -> None:
+ root_dir: str = '',
+ version = image.get_running_image()) -> None:
"""Update Kernel custom cmdline options"""
if not root_dir:
root_dir = disk.find_persistence()
- version = image.get_running_image()
-
boot_opts_current = grub.get_boot_opts(version, root_dir)
boot_opts_proposed = grub.BOOT_OPTS_STEM + f'{version} {cmdline_options}'
diff --git a/python/vyos/template.py b/python/vyos/template.py
index e75db1a8d..d79e1183f 100755
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -390,28 +390,6 @@ def compare_netmask(netmask1, netmask2):
except:
return False
-@register_filter('isc_static_route')
-def isc_static_route(subnet, router):
- # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
- # Option format is:
- # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
- # where bytes with the value 0 are omitted.
- from ipaddress import ip_network
- net = ip_network(subnet)
- # add netmask
- string = str(net.prefixlen) + ','
- # add network bytes
- if net.prefixlen:
- width = net.prefixlen // 8
- if net.prefixlen % 8:
- width += 1
- string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
-
- # add router bytes
- string += ','.join(router.split('.'))
-
- return string
-
@register_filter('is_file')
def is_file(filename):
if os.path.exists(filename):
@@ -881,10 +859,77 @@ def kea_high_availability_json(config):
return dumps(data)
+@register_filter('kea_dynamic_dns_update_main_json')
+def kea_dynamic_dns_update_main_json(config):
+ from vyos.kea import kea_parse_ddns_settings
+ from json import dumps
+
+ data = kea_parse_ddns_settings(config)
+
+ if len(data) == 0:
+ return ''
+
+ return dumps(data, indent=8)[1:-1] + ','
+
+@register_filter('kea_dynamic_dns_update_tsig_key_json')
+def kea_dynamic_dns_update_tsig_key_json(config):
+ from vyos.kea import kea_parse_tsig_algo
+ from json import dumps
+ out = []
+
+ if 'tsig_key' not in config:
+ return dumps(out)
+
+ tsig_keys = config['tsig_key']
+
+ for tsig_key_name, tsig_key_config in tsig_keys.items():
+ tsig_key = {
+ 'name': tsig_key_name,
+ 'algorithm': kea_parse_tsig_algo(tsig_key_config['algorithm']),
+ 'secret': tsig_key_config['secret']
+ }
+ out.append(tsig_key)
+
+ return dumps(out, indent=12)
+
+@register_filter('kea_dynamic_dns_update_domains')
+def kea_dynamic_dns_update_domains(config, type_key):
+ from json import dumps
+ out = []
+
+ if type_key not in config:
+ return dumps(out)
+
+ domains = config[type_key]
+
+ for domain_name, domain_config in domains.items():
+ domain = {
+ 'name': domain_name,
+
+ }
+ if 'key_name' in domain_config:
+ domain['key-name'] = domain_config['key_name']
+
+ if 'dns_server' in domain_config:
+ dns_servers = []
+ for dns_server_config in domain_config['dns_server'].values():
+ dns_server = {
+ 'ip-address': dns_server_config['address']
+ }
+ if 'port' in dns_server_config:
+ dns_server['port'] = int(dns_server_config['port'])
+ dns_servers.append(dns_server)
+ domain['dns-servers'] = dns_servers
+
+ out.append(domain)
+
+ return dumps(out, indent=12)
+
@register_filter('kea_shared_network_json')
def kea_shared_network_json(shared_networks):
from vyos.kea import kea_parse_options
from vyos.kea import kea_parse_subnet
+ from vyos.kea import kea_parse_ddns_settings
from json import dumps
out = []
@@ -895,9 +940,13 @@ def kea_shared_network_json(shared_networks):
network = {
'name': name,
'authoritative': ('authoritative' in config),
- 'subnet4': []
+ 'subnet4': [],
+ 'user-context': {}
}
+ if 'dynamic_dns_update' in config:
+ network.update(kea_parse_ddns_settings(config['dynamic_dns_update']))
+
if 'option' in config:
network['option-data'] = kea_parse_options(config['option'])
@@ -907,6 +956,9 @@ def kea_shared_network_json(shared_networks):
if 'bootfile_server' in config['option']:
network['next-server'] = config['option']['bootfile_server']
+ if 'ping_check' in config:
+ network['user-context']['enable-ping-check'] = True
+
if 'subnet' in config:
for subnet, subnet_config in config['subnet'].items():
if 'disable' in subnet_config:
diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py
new file mode 100644
index 000000000..506095625
--- /dev/null
+++ b/python/vyos/vyconf_session.py
@@ -0,0 +1,123 @@
+# 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/>.
+#
+#
+
+import tempfile
+import shutil
+from functools import wraps
+from typing import Type
+
+from vyos.proto import vyconf_client
+from vyos.migrate import ConfigMigrate
+from vyos.migrate import ConfigMigrateError
+from vyos.component_version import append_system_version
+
+
+def output(o):
+ out = ''
+ for res in (o.output, o.error, o.warning):
+ if res is not None:
+ out = out + res
+ return out
+
+
+class VyconfSession:
+ def __init__(self, token: str = None, on_error: Type[Exception] = None):
+ if token is None:
+ out = vyconf_client.send_request('setup_session')
+ self.__token = out.output
+ else:
+ self.__token = token
+
+ self.on_error = on_error
+
+ @staticmethod
+ def raise_exception(f):
+ @wraps(f)
+ def wrapped(self, *args, **kwargs):
+ if self.on_error is None:
+ return f(self, *args, **kwargs)
+ o, e = f(self, *args, **kwargs)
+ if e:
+ raise self.on_error(o)
+ return o, e
+
+ return wrapped
+
+ @raise_exception
+ 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
+
+ @raise_exception
+ 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
+
+ @raise_exception
+ def commit(self) -> tuple[str, int]:
+ out = vyconf_client.send_request('commit', token=self.__token)
+ return output(out), out.status
+
+ @raise_exception
+ 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)
+
+ @raise_exception
+ def load_config(self, file: str, migrate: bool = False) -> tuple[str, int]:
+ # pylint: disable=consider-using-with
+ if migrate:
+ tmp = tempfile.NamedTemporaryFile()
+ shutil.copy2(file, tmp.name)
+ config_migrate = ConfigMigrate(tmp.name)
+ try:
+ config_migrate.run()
+ except ConfigMigrateError as e:
+ tmp.close()
+ return repr(e), 1
+ file = tmp.name
+ else:
+ tmp = ''
+
+ out = vyconf_client.send_request('load', token=self.__token, location=file)
+ if tmp:
+ tmp.close()
+
+ return 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
+
+ @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)}')