summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2026-09-01 17:14:00 +0200
committerGitHub <noreply@github.com>2026-09-01 17:14:00 +0200
commit6d2523edeed89e0d968df13b7ec656de5bbd2952 (patch)
treeb896d6b43728343ad64ca2ee7c2ea5227a8fdf3c /python
parent3623796a0577659e789e4344daede98dad479233 (diff)
parent7e1460e8fb43f62caf0611b5cc9900b1d09f76e7 (diff)
downloadvyos-1x-6d2523edeed89e0d968df13b7ec656de5bbd2952.tar.gz
vyos-1x-6d2523edeed89e0d968df13b7ec656de5bbd2952.zip
Merge pull request #5434 from c-po/python-startup-time-improvements
T9261: Python interpreter startup makes the CLI sluggish on low-cache CPUs
Diffstat (limited to 'python')
-rw-r--r--python/vyos/configsource.py11
-rw-r--r--python/vyos/configtree.py463
-rw-r--r--python/vyos/ethtool.py8
-rw-r--r--python/vyos/ifconfig/ethernet.py6
-rw-r--r--python/vyos/ifconfig/interface.py53
-rw-r--r--python/vyos/ifconfig/operational.py3
-rw-r--r--python/vyos/ifconfig/vrrp.py3
-rw-r--r--python/vyos/ifconfig/wireguard.py13
-rw-r--r--python/vyos/opmode.py11
-rw-r--r--python/vyos/qos/base.py9
-rw-r--r--python/vyos/referencetree.py42
-rwxr-xr-xpython/vyos/template.py10
-rw-r--r--python/vyos/utils/__init__.py58
-rw-r--r--python/vyos/utils/auth.py13
-rw-r--r--python/vyos/version.py11
-rw-r--r--python/vyos/vyconf_session.py11
16 files changed, 348 insertions, 377 deletions
diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py
index b54f6283a..ce9b9a62a 100644
--- a/python/vyos/configsource.py
+++ b/python/vyos/configsource.py
@@ -21,8 +21,6 @@ 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
@@ -332,6 +330,13 @@ class ConfigSourceVyconfSession(ConfigSource):
pid = os.environ.get('SESSION_PID', '')
self.pid = int(pid) if pid else os.getppid()
+ # Imported here rather than at module scope: vyos.vyconf_session pulls
+ # in vyos.proto.vyconf_client and with it google.protobuf, which every
+ # consumer of vyos.config would otherwise pay for on import even when
+ # the vyconf backend is not in use.
+ from vyos.vyconf_session import VyconfSession
+ from vyos.vyconf_session import VyconfSessionError
+
self._vyconf_session = VyconfSession(pid=self.pid)
try:
out = self._vyconf_session.get_config()
@@ -366,6 +371,8 @@ class ConfigSourceVyconfSession(ConfigSource):
Returns:
True if the config session has uncommitted changes, False otherwise.
"""
+ from vyos.vyconf_session import VyconfSessionError
+
try:
return self._vyconf_session.session_changed()
except VyconfSessionError:
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 6791b1c29..90e889692 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -18,6 +18,7 @@ import json
import logging
from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool
+from functools import lru_cache
from typing import TYPE_CHECKING
from pathlib import Path
@@ -31,6 +32,105 @@ BUILD_PATH = '/tmp/libvyosconfig/_build/libvyosconfig.so'
INSTALL_PATH = '/usr/lib/libvyosconfig.so.0'
LIBPATH = BUILD_PATH if os.path.isfile(BUILD_PATH) else INSTALL_PATH
+# C symbol -> (argtypes, restype). A restype of None means "leave the ctypes
+# default (c_int)", which is what these entry points relied on before.
+_PROTOTYPES = {
+ 'from_string': ([c_char_p], c_void_p),
+ 'get_error': ([], c_char_p),
+ 'to_string': ([c_void_p, c_bool], c_char_p),
+ 'to_commands': ([c_void_p, c_char_p], c_char_p),
+ 'read_internal': ([c_char_p], c_void_p),
+ 'write_internal': ([c_void_p, c_char_p], None),
+ 'read_internal_string': ([c_char_p], c_void_p),
+ 'write_internal_string': ([c_void_p], c_char_p),
+ 'to_json': ([c_void_p], c_char_p),
+ 'to_json_ast': ([c_void_p], c_char_p),
+ 'create_node': ([c_void_p, c_char_p], c_int),
+ 'set_add_value': ([c_void_p, c_char_p, c_char_p], c_int),
+ 'delete_value': ([c_void_p, c_char_p, c_char_p], c_int),
+ 'delete_node': ([c_void_p, c_char_p], c_int),
+ 'rename_node': ([c_void_p, c_char_p, c_char_p], c_int),
+ 'copy_node': ([c_void_p, c_char_p, c_char_p], c_int),
+ 'set_replace_value': ([c_void_p, c_char_p, c_char_p], c_int),
+ 'set_valueless': ([c_void_p, c_char_p], c_int),
+ 'exists': ([c_void_p, c_char_p], c_int),
+ 'value_exists': ([c_void_p, c_char_p, c_char_p], c_int),
+ 'list_nodes': ([c_void_p, c_char_p], c_char_p),
+ 'return_value': ([c_void_p, c_char_p], c_char_p),
+ 'return_values': ([c_void_p, c_char_p], c_char_p),
+ 'is_tag': ([c_void_p, c_char_p], c_int),
+ 'set_tag': ([c_void_p, c_char_p, c_bool], c_int),
+ 'is_leaf': ([c_void_p, c_char_p], c_bool),
+ 'set_leaf': ([c_void_p, c_char_p, c_bool], c_int),
+ # third argument 'with_node' was passed unchecked before
+ 'get_subtree': ([c_void_p, c_char_p, c_bool], c_void_p),
+ 'destroy': ([c_void_p], None),
+ 'equal': ([c_void_p, c_void_p], c_bool),
+ 'config_dict': (
+ [c_void_p, c_void_p, c_void_p, c_char_p, c_bool, c_bool],
+ c_char_p,
+ ),
+ 'diff_tree': ([c_char_p, c_void_p, c_void_p], c_void_p),
+ 'diff_compare': ([c_bool, c_char_p, c_void_p, c_void_p], c_char_p),
+ 'diff_show': ([c_void_p, c_void_p, c_void_p, c_char_p], c_char_p),
+ 'tree_union': ([c_void_p, c_void_p], c_void_p),
+ 'tree_merge': ([c_bool, c_void_p, c_void_p], c_void_p),
+ 'mask_inclusive': ([c_void_p, c_void_p], c_void_p),
+ 'mask_exclusive': ([c_void_p, c_void_p], c_void_p),
+ 'subtree_from_partial': ([c_void_p, c_void_p, c_void_p, c_char_p], c_void_p),
+ 'reference_tree_to_json': ([c_char_p, c_char_p, c_char_p], None),
+ 'merge_reference_tree_cache': ([c_char_p, c_char_p, c_char_p], None),
+ 'interface_definitions_to_cache': ([c_char_p, c_char_p], None),
+ 'reference_tree_cache_to_json': ([c_char_p, c_char_p], None),
+ # returns a tree pointer; without an explicit restype ctypes defaulted to
+ # c_int and truncated it to 32 bits
+ 'validate_tree_filter': ([c_void_p, c_char_p, c_char_p], c_void_p),
+ # vyos.referencetree
+ 'read_internal_string_reference_tree': ([c_char_p], c_void_p),
+ 'write_internal_reference_tree': ([c_void_p, c_char_p], None),
+ 'to_json_reference_tree': ([c_void_p], c_char_p),
+}
+
+
+class _Lib:
+ """Lazily resolving, prototype-declaring wrapper around libvyosconfig.
+
+ A symbol is looked up and given its argtypes/restype on first use, then
+ cached as an instance attribute so __getattr__ is bypassed from then on.
+ Resolution stays lazy on purpose: not every build of the library exports
+ every entry point, and the previous per-instance binding code only failed
+ on symbols that were actually used.
+ """
+
+ def __init__(self, libpath):
+ self.__dict__['_lib'] = cdll.LoadLibrary(libpath)
+
+ def __getattr__(self, name):
+ try:
+ argtypes, restype = _PROTOTYPES[name]
+ except KeyError:
+ raise AttributeError(
+ f'{name} is not a known libvyosconfig entry point'
+ ) from None
+ func = getattr(self._lib, name)
+ func.argtypes = argtypes
+ if restype is not None:
+ func.restype = restype
+ setattr(self, name, func)
+ return func
+
+
+@lru_cache(maxsize=None)
+def get_lib(libpath=LIBPATH):
+ """Load libvyosconfig once per process.
+
+ Every ConfigTree/DiffTree used to re-open the library and re-assign all
+ argtypes/restypes in its constructor; the result is cached here instead.
+ Loading stays lazy so that importing this module does not require the
+ shared object to be present.
+ """
+ return _Lib(libpath)
+
def replace_backslash(s, search, replace):
"""Modify quoted strings containing backslashes not of escape sequences"""
@@ -95,137 +195,7 @@ class ConfigTree:
arg = [(k, v) for (k, v) in zip(arg_names, args) if v is not None][0]
self.__config = None
- self.__lib = cdll.LoadLibrary(libpath)
-
- # Import functions
- self.__from_string = self.__lib.from_string
- self.__from_string.argtypes = [c_char_p]
- self.__from_string.restype = c_void_p
-
- self.__get_error = self.__lib.get_error
- self.__get_error.argtypes = []
- self.__get_error.restype = c_char_p
-
- self.__to_string = self.__lib.to_string
- self.__to_string.argtypes = [c_void_p, c_bool]
- self.__to_string.restype = c_char_p
-
- self.__to_commands = self.__lib.to_commands
- self.__to_commands.argtypes = [c_void_p, c_char_p]
- self.__to_commands.restype = c_char_p
-
- self.__read_internal = self.__lib.read_internal
- self.__read_internal.argtypes = [c_char_p]
- self.__read_internal.restype = c_void_p
-
- self.__write_internal = self.__lib.write_internal
- self.__write_internal.argtypes = [c_void_p, c_char_p]
-
- self.__read_internal_string = self.__lib.read_internal_string
- self.__read_internal_string.argtypes = [c_char_p]
- self.__read_internal_string.restype = c_void_p
-
- self.__write_internal_string = self.__lib.write_internal_string
- self.__write_internal_string.argtypes = [c_void_p]
- self.__write_internal_string.restype = c_char_p
-
- self.__to_json = self.__lib.to_json
- self.__to_json.argtypes = [c_void_p]
- self.__to_json.restype = c_char_p
-
- self.__to_json_ast = self.__lib.to_json_ast
- self.__to_json_ast.argtypes = [c_void_p]
- self.__to_json_ast.restype = c_char_p
-
- self.__create_node = self.__lib.create_node
- self.__create_node.argtypes = [c_void_p, c_char_p]
- self.__create_node.restype = c_int
-
- self.__set_add_value = self.__lib.set_add_value
- self.__set_add_value.argtypes = [c_void_p, c_char_p, c_char_p]
- self.__set_add_value.restype = c_int
-
- self.__delete_value = self.__lib.delete_value
- self.__delete_value.argtypes = [c_void_p, c_char_p, c_char_p]
- self.__delete_value.restype = c_int
-
- self.__delete = self.__lib.delete_node
- self.__delete.argtypes = [c_void_p, c_char_p]
- self.__delete.restype = c_int
-
- self.__rename = self.__lib.rename_node
- self.__rename.argtypes = [c_void_p, c_char_p, c_char_p]
- self.__rename.restype = c_int
-
- self.__copy = self.__lib.copy_node
- self.__copy.argtypes = [c_void_p, c_char_p, c_char_p]
- self.__copy.restype = c_int
-
- self.__set_replace_value = self.__lib.set_replace_value
- self.__set_replace_value.argtypes = [c_void_p, c_char_p, c_char_p]
- self.__set_replace_value.restype = c_int
-
- self.__set_valueless = self.__lib.set_valueless
- self.__set_valueless.argtypes = [c_void_p, c_char_p]
- self.__set_valueless.restype = c_int
-
- self.__exists = self.__lib.exists
- self.__exists.argtypes = [c_void_p, c_char_p]
- self.__exists.restype = c_int
-
- self.__value_exists = self.__lib.value_exists
- self.__value_exists.argtypes = [c_void_p, c_char_p, c_char_p]
- self.__value_exists.restype = c_int
-
- self.__list_nodes = self.__lib.list_nodes
- self.__list_nodes.argtypes = [c_void_p, c_char_p]
- self.__list_nodes.restype = c_char_p
-
- self.__return_value = self.__lib.return_value
- self.__return_value.argtypes = [c_void_p, c_char_p]
- self.__return_value.restype = c_char_p
-
- self.__return_values = self.__lib.return_values
- self.__return_values.argtypes = [c_void_p, c_char_p]
- self.__return_values.restype = c_char_p
-
- self.__is_tag = self.__lib.is_tag
- self.__is_tag.argtypes = [c_void_p, c_char_p]
- self.__is_tag.restype = c_int
-
- self.__set_tag = self.__lib.set_tag
- self.__set_tag.argtypes = [c_void_p, c_char_p, c_bool]
- self.__set_tag.restype = c_int
-
- self.__is_leaf = self.__lib.is_leaf
- self.__is_leaf.argtypes = [c_void_p, c_char_p]
- self.__is_leaf.restype = c_bool
-
- self.__set_leaf = self.__lib.set_leaf
- self.__set_leaf.argtypes = [c_void_p, c_char_p, c_bool]
- self.__set_leaf.restype = c_int
-
- self.__get_subtree = self.__lib.get_subtree
- self.__get_subtree.argtypes = [c_void_p, c_char_p]
- self.__get_subtree.restype = c_void_p
-
- self.__destroy = self.__lib.destroy
- self.__destroy.argtypes = [c_void_p]
-
- self.__equal = self.__lib.equal
- self.__equal.argtypes = [c_void_p, c_void_p]
- self.__equal.restype = c_bool
-
- self.__config_dict = self.__lib.config_dict
- self.__config_dict.argtypes = [
- c_void_p,
- c_void_p,
- c_void_p,
- c_char_p,
- c_bool,
- c_bool,
- ]
- self.__config_dict.restype = c_char_p
+ self.__lib = get_lib(libpath)
match arg:
case ('address', address):
@@ -236,18 +206,18 @@ class ConfigTree:
cache_string = Path(internal).read_bytes()
except OSError as e:
raise ValueError(f'Failed to read internal cache file: {e}')
- config = self.__read_internal_string(cache_string)
+ config = self.__lib.read_internal_string(cache_string)
if config is None:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ValueError(
f'Failed to read internal representation from file {internal}: {msg}'
)
self.__config = config
self.__version = ''
case ('internal_string', internal_string):
- config = self.__read_internal_string(internal_string.encode())
+ config = self.__lib.read_internal_string(internal_string.encode())
if config is None:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ValueError(
f'Failed to read internal representation from string: {msg}'
)
@@ -256,9 +226,9 @@ class ConfigTree:
case ('config_string', config_string):
config_section, version_section = extract_version(config_string)
config_section = escape_backslash(config_section)
- config = self.__from_string(config_section.encode())
+ config = self.__lib.from_string(config_section.encode())
if config is None:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ValueError(f'Failed to parse config: {msg}')
self.__config = config
self.__version = version_section
@@ -286,11 +256,11 @@ class ConfigTree:
def __del__(self):
if self.__config is not None:
- self.__destroy(self.__config)
+ self.__lib.destroy(self.__config)
def __eq__(self, other):
if isinstance(other, ConfigTree):
- return self.__equal(self.get_tree(), other.get_tree())
+ return self.__lib.equal(self.get_tree(), other.get_tree())
return False
def __str__(self):
@@ -303,18 +273,18 @@ class ConfigTree:
return self.__version
def write_cache(self, file_name):
- cache_string = self.__write_internal_string(self.get_tree())
+ cache_string = self.__lib.write_internal_string(self.get_tree())
try:
Path(file_name).write_bytes(cache_string)
except OSError as e:
raise ValueError(f'Failed to write internal cache file: {e}')
def write_internal_string(self) -> str:
- res = self.__write_internal_string(self.get_tree())
+ res = self.__lib.write_internal_string(self.get_tree())
return res.decode()
def to_string(self, ordered_values=False, no_version=False):
- config_string = self.__to_string(self.__config, ordered_values).decode()
+ config_string = self.__lib.to_string(self.__config, ordered_values).decode()
config_string = unescape_backslash(config_string)
if no_version:
return config_string
@@ -322,23 +292,23 @@ class ConfigTree:
return config_string
def to_commands(self, op='set'):
- commands = self.__to_commands(self.__config, op.encode()).decode()
+ commands = self.__lib.to_commands(self.__config, op.encode()).decode()
commands = unescape_backslash(commands)
return commands
def to_json(self):
- return self.__to_json(self.__config).decode()
+ return self.__lib.to_json(self.__config).decode()
def to_json_ast(self):
- return self.__to_json_ast(self.__config).decode()
+ return self.__lib.to_json_ast(self.__config).decode()
def create_node(self, path):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__create_node(self.__config, path_str)
+ res = self.__lib.create_node(self.__config, path_str)
if res != 0:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ConfigTreeError(f'{msg}: {path}')
def set(self, path, value=None, replace=True):
@@ -354,17 +324,17 @@ class ConfigTree:
path_str = ' '.join(map(str, path)).encode()
if value is None:
- res = self.__set_valueless(self.__config, path_str)
+ res = self.__lib.set_valueless(self.__config, path_str)
else:
if replace:
- res = self.__set_replace_value(
+ res = self.__lib.set_replace_value(
self.__config, path_str, str(value).encode()
)
else:
- res = self.__set_add_value(self.__config, path_str, str(value).encode())
+ res = self.__lib.set_add_value(self.__config, path_str, str(value).encode())
if res != 0:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ConfigTreeError(
f'{msg}: path "{path}" value "{value}" replace "{replace}"'
)
@@ -378,9 +348,9 @@ class ConfigTree:
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__delete(self.__config, path_str)
+ res = self.__lib.delete_node(self.__config, path_str)
if res != 0:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ConfigTreeError(f'{msg}: path "{path}"')
if self.__migration:
@@ -390,9 +360,9 @@ class ConfigTree:
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__delete_value(self.__config, path_str, value.encode())
+ res = self.__lib.delete_value(self.__config, path_str, value.encode())
if res != 0:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ConfigTreeError(f'{msg}: path "{path}" value "{value}"')
if self.__migration:
@@ -408,9 +378,9 @@ class ConfigTree:
if self.exists(new_path):
raise ConfigTreeError(f'Name {new_name} already exists')
- res = self.__rename(self.__config, path_str, newname_str)
+ res = self.__lib.rename_node(self.__config, path_str, newname_str)
if res != 0:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ConfigTreeError(f'{msg}: {path}')
if self.__migration:
@@ -427,9 +397,9 @@ class ConfigTree:
# Check if a node with intended new name already exists
if self.exists(new_path):
raise ConfigTreeError()
- res = self.__copy(self.__config, oldpath_str, newpath_str)
+ res = self.__lib.copy_node(self.__config, oldpath_str, newpath_str)
if res != 0:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ConfigTreeError(msg)
if self.__migration:
@@ -441,21 +411,21 @@ class ConfigTree:
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__exists(self.__config, path_str)
+ res = self.__lib.exists(self.__config, path_str)
return bool(res)
def value_exists(self, path, value):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__value_exists(self.__config, path_str, value.encode())
+ res = self.__lib.value_exists(self.__config, path_str, value.encode())
return bool(res)
def list_nodes(self, path, path_must_exist=True):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res_json = self.__list_nodes(self.__config, path_str).decode()
+ res_json = self.__lib.list_nodes(self.__config, path_str).decode()
res = json.loads(res_json)
if res is None:
@@ -468,7 +438,7 @@ class ConfigTree:
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res_json = self.__return_value(self.__config, path_str).decode()
+ res_json = self.__lib.return_value(self.__config, path_str).decode()
res = json.loads(res_json)
if res is None:
@@ -479,7 +449,7 @@ class ConfigTree:
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res_json = self.__return_values(self.__config, path_str).decode()
+ res_json = self.__lib.return_values(self.__config, path_str).decode()
res = json.loads(res_json)
if res is None:
@@ -490,41 +460,41 @@ class ConfigTree:
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__is_tag(self.__config, path_str)
+ res = self.__lib.is_tag(self.__config, path_str)
return bool(res)
def set_tag(self, path, value=True):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__set_tag(self.__config, path_str, value)
+ res = self.__lib.set_tag(self.__config, path_str, value)
if res == 0:
return True
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ConfigTreeError(f'{msg}: {path}')
def is_leaf(self, path):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__is_leaf(self.__config, path_str)
+ res = self.__lib.is_leaf(self.__config, path_str)
return bool(res)
def set_leaf(self, path, value):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__set_leaf(self.__config, path_str, value)
+ res = self.__lib.set_leaf(self.__config, path_str, value)
if res == 0:
return True
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ConfigTreeError(f'{msg}: {path}')
def get_subtree(self, path, with_node=False):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__get_subtree(self.__config, path_str, with_node)
+ res = self.__lib.get_subtree(self.__config, path_str, with_node)
subt = ConfigTree(address=res)
return subt
@@ -534,7 +504,7 @@ class ConfigTree:
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res_json = self.__config_dict(
+ res_json = self.__lib.config_dict(
self.__config,
ref_tree.get_tree(),
mask.get_tree(),
@@ -561,18 +531,12 @@ def diff_compare(left, right, path=None, commands=False, libpath=LIBPATH):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- __lib = cdll.LoadLibrary(libpath)
- __diff_compare = __lib.diff_compare
- __diff_compare.argtypes = [c_bool, c_char_p, c_void_p, c_void_p]
- __diff_compare.restype = c_char_p
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
+ lib = get_lib(libpath)
- res = __diff_compare(commands, path_str, left.get_tree(), right.get_tree())
+ res = lib.diff_compare(commands, path_str, left.get_tree(), right.get_tree())
res = res.decode()
if res == '#1@':
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
res = unescape_backslash(res)
@@ -587,18 +551,12 @@ def diff_show(rt, left, right, path=None, libpath=LIBPATH):
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- __lib = cdll.LoadLibrary(libpath)
- __diff_show = __lib.diff_show
- __diff_show.argtypes = [c_void_p, c_void_p, c_void_p, c_char_p]
- __diff_show.restype = c_char_p
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
+ lib = get_lib(libpath)
- res = __diff_show(rt.get_tree(), left.get_tree(), right.get_tree(), path_str)
+ res = lib.diff_show(rt.get_tree(), left.get_tree(), right.get_tree(), path_str)
res = res.decode()
if res == '#1@':
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
res = unescape_backslash(res)
@@ -613,15 +571,9 @@ def union(left, right, libpath=LIBPATH):
if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
raise TypeError('Arguments must be instances of ConfigTree')
- __lib = cdll.LoadLibrary(libpath)
- __tree_union = __lib.tree_union
- __tree_union.argtypes = [c_void_p, c_void_p]
- __tree_union.restype = c_void_p
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
+ lib = get_lib(libpath)
- res = __tree_union(left.get_tree(), right.get_tree())
+ res = lib.tree_union(left.get_tree(), right.get_tree())
tree = ConfigTree(address=res)
return tree
@@ -635,15 +587,9 @@ def merge(left, right, destructive=False, libpath=LIBPATH):
if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
raise TypeError('Arguments must be instances of ConfigTree')
- __lib = cdll.LoadLibrary(libpath)
- __tree_merge = __lib.tree_merge
- __tree_merge.argtypes = [c_bool, c_void_p, c_void_p]
- __tree_merge.restype = c_void_p
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
+ lib = get_lib(libpath)
- res = __tree_merge(destructive, left.get_tree(), right.get_tree())
+ res = lib.tree_merge(destructive, left.get_tree(), right.get_tree())
tree = ConfigTree(address=res)
return tree
@@ -655,19 +601,13 @@ def mask_inclusive(left, right, libpath=LIBPATH):
raise TypeError('Arguments must be instances of ConfigTree')
try:
- __lib = cdll.LoadLibrary(libpath)
- __mask_inclusive = __lib.mask_inclusive
- __mask_inclusive.argtypes = [c_void_p, c_void_p]
- __mask_inclusive.restype = c_void_p
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
-
- res = __mask_inclusive(left.get_tree(), right.get_tree())
+ lib = get_lib(libpath)
+
+ res = lib.mask_inclusive(left.get_tree(), right.get_tree())
except Exception as e:
raise ConfigTreeError(e)
if not res:
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
tree = ConfigTree(address=res)
@@ -681,19 +621,13 @@ def mask_exclusive(left, right, libpath=LIBPATH):
raise TypeError('Arguments must be instances of ConfigTree')
try:
- __lib = cdll.LoadLibrary(libpath)
- __mask_exclusive = __lib.mask_exclusive
- __mask_exclusive.argtypes = [c_void_p, c_void_p]
- __mask_exclusive.restype = c_void_p
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
-
- res = __mask_exclusive(left.get_tree(), right.get_tree())
+ lib = get_lib(libpath)
+
+ res = lib.mask_exclusive(left.get_tree(), right.get_tree())
except Exception as e:
raise ConfigTreeError(e)
if not res:
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
tree = ConfigTree(address=res)
@@ -741,15 +675,9 @@ def subtree_from_partial(
path_str = ' '.join(map(str, path)).encode()
try:
- __lib = cdll.LoadLibrary(libpath)
- __subtree_from_partial = __lib.subtree_from_partial
- __subtree_from_partial.argtypes = [c_void_p, c_void_p, c_void_p, c_char_p]
- __subtree_from_partial.restype = c_void_p
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
-
- res = __subtree_from_partial(
+ lib = get_lib(libpath)
+
+ res = lib.subtree_from_partial(
reference_tree.get_tree(),
config_tree.get_tree(),
start.get_tree(),
@@ -758,7 +686,7 @@ def subtree_from_partial(
except Exception as e:
raise ConfigTreeError(e)
if not res:
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
tree = ConfigTree(address=res)
@@ -769,72 +697,52 @@ def subtree_from_partial(
def reference_tree_to_json(from_dir, to_file, internal_cache='', libpath=LIBPATH):
# pylint: disable=raise-missing-from
try:
- __lib = cdll.LoadLibrary(libpath)
- __reference_tree_to_json = __lib.reference_tree_to_json
- __reference_tree_to_json.argtypes = [c_char_p, c_char_p, c_char_p]
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
- res = __reference_tree_to_json(
+ lib = get_lib(libpath)
+ res = lib.reference_tree_to_json(
internal_cache.encode(), from_dir.encode(), to_file.encode()
)
except Exception as e:
raise ConfigTreeError(e)
if res == 1:
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
def merge_reference_tree_cache(cache_dir, primary_name, result_name, libpath=LIBPATH):
# pylint: disable=raise-missing-from
try:
- __lib = cdll.LoadLibrary(libpath)
- __merge_reference_tree_cache = __lib.merge_reference_tree_cache
- __merge_reference_tree_cache.argtypes = [c_char_p, c_char_p, c_char_p]
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
- res = __merge_reference_tree_cache(
+ lib = get_lib(libpath)
+ res = lib.merge_reference_tree_cache(
cache_dir.encode(), primary_name.encode(), result_name.encode()
)
except Exception as e:
raise ConfigTreeError(e)
if res == 1:
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
def interface_definitions_to_cache(from_dir, cache_path, libpath=LIBPATH):
# pylint: disable=raise-missing-from
try:
- __lib = cdll.LoadLibrary(libpath)
- __interface_definitions_to_cache = __lib.interface_definitions_to_cache
- __interface_definitions_to_cache.argtypes = [c_char_p, c_char_p]
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
- res = __interface_definitions_to_cache(from_dir.encode(), cache_path.encode())
+ lib = get_lib(libpath)
+ res = lib.interface_definitions_to_cache(from_dir.encode(), cache_path.encode())
except Exception as e:
raise ConfigTreeError(e)
if res == 1:
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
def reference_tree_cache_to_json(cache_path, render_file, libpath=LIBPATH):
# pylint: disable=raise-missing-from
try:
- __lib = cdll.LoadLibrary(libpath)
- __reference_tree_cache_to_json = __lib.reference_tree_cache_to_json
- __reference_tree_cache_to_json.argtypes = [c_char_p, c_char_p]
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
- res = __reference_tree_cache_to_json(cache_path.encode(), render_file.encode())
+ lib = get_lib(libpath)
+ res = lib.reference_tree_cache_to_json(cache_path.encode(), render_file.encode())
except Exception as e:
raise ConfigTreeError(e)
if res == 1:
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
raise ConfigTreeError(msg)
@@ -847,19 +755,14 @@ def validate_tree_filter(
):
# pylint: disable=raise-missing-from
try:
- __lib = cdll.LoadLibrary(libpath)
- __validate_tree_filter = __lib.validate_tree_filter
- __validate_tree_filter.argtypes = [c_void_p, c_char_p, c_char_p]
- __get_error = __lib.get_error
- __get_error.argtypes = []
- __get_error.restype = c_char_p
- res = __validate_tree_filter(
+ lib = get_lib(libpath)
+ res = lib.validate_tree_filter(
config_tree.get_tree(), cache_path.encode(), validator_dir.encode()
)
except Exception as e:
raise ConfigTreeError(e)
- msg = __get_error().decode()
+ msg = lib.get_error().decode()
tree = ConfigTree(address=res)
return tree, msg
@@ -896,16 +799,12 @@ class DiffTree:
self.left = left
self.right = right
- self.__lib = cdll.LoadLibrary(libpath)
-
- self.__diff_tree = self.__lib.diff_tree
- self.__diff_tree.argtypes = [c_char_p, c_void_p, c_void_p]
- self.__diff_tree.restype = c_void_p
+ self.__lib = get_lib(libpath)
check_path(path)
path_str = ' '.join(map(str, path)).encode()
- res = self.__diff_tree(path_str, left.get_tree(), right.get_tree())
+ res = self.__lib.diff_tree(path_str, left.get_tree(), right.get_tree())
# full diff config_tree and python dict representation
self.full = ConfigTree(address=res)
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index cbc13a0e9..66dc6bb89 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -19,8 +19,6 @@ import contextlib
from json import loads
from vyos.utils.network import interface_exists
from vyos.utils.process import popen
-from vyos.netlink import coalesce
-from vyos.netlink import timestamp
# These drivers do not support using ethtool to change the speed, duplex, or
# flow control settings
@@ -127,6 +125,12 @@ class Ethtool:
if not bool(err):
self._channels = out.lower()
+ # Imported here rather than at module scope: vyos.netlink.* imports
+ # pyroute2, which is by far the largest single cost on the vyos.ifconfig
+ # import path. Only code that actually constructs an Ethtool needs it.
+ from vyos.netlink import coalesce
+ from vyos.netlink import timestamp
+
# Get information about NIC coalesce settings
with contextlib.suppress(coalesce.CoalesceError, coalesce.GeneralNetlinkError):
self._coalesce = coalesce.get_coalesce(ifname)
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 84c002550..bb5119f77 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -19,7 +19,6 @@ from glob import glob
from vyos.base import Warning
from vyos.ethtool import Ethtool
-from vyos.netlink import coalesce
from vyos.ifconfig import Section
from vyos.ifconfig.interface import Interface
from vyos.utils.dict import dict_search
@@ -512,6 +511,11 @@ class EthernetIf(Interface):
if not params:
return None
+ # Imported here rather than at module scope: vyos.netlink.coalesce
+ # imports pyroute2, which every consumer of vyos.ifconfig would
+ # otherwise pay for on import.
+ from vyos.netlink import coalesce
+
# Override boolean parameters to true if they exist and supported by NIC driver
for boolean_param in coalesce.get_all_params(boolean=True):
supported = self.ethtool.check_coalesce(boolean_param)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index e62812d31..3a5efec8c 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -16,7 +16,6 @@
import os
import re
import json
-import jmespath
from copy import deepcopy
from glob import glob
@@ -26,17 +25,11 @@ from ipaddress import IPv6Interface
from netifaces import ifaddresses # pylint: disable = no-name-in-module
from socket import AF_INET
from socket import AF_INET6
-from netaddr import EUI
-from netaddr import mac_unix_expanded
from vyos.configdict import list_diff
from vyos.configdict import dict_merge
from vyos.configdict import get_vlan_ids
from vyos.defaults import directories
-from vyos.pki import find_chain
-from vyos.pki import encode_certificate
-from vyos.pki import load_certificate
-from vyos.pki import wrap_private_key
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.template import render
@@ -68,6 +61,18 @@ from vyos.ifconfig import Section
link_local_prefix = 'fe80::/64'
+def _jmes_search(expression, data):
+ """jmespath.search with a deferred import.
+
+ The lookups below live in lambdas evaluated at call time, so importing
+ jmespath at module scope would put it on the vyos.ifconfig import path for
+ no reason.
+ """
+ import jmespath
+
+ return jmespath.search(expression, data)
+
+
class Interface(Control):
# This is the class which will be used to create
# self.operational, it allows subclasses, such as
@@ -92,39 +97,39 @@ class Interface(Control):
_command_get = {
'admin_state': {
'shellcmd': 'ip -json link show dev {ifname}',
- 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down',
+ 'format': lambda j: 'up' if 'UP' in _jmes_search('[*].flags | [0]', json.loads(j)) else 'down',
},
'alias': {
'shellcmd': 'ip -json -detail link list dev {ifname}',
- 'format': lambda j: jmespath.search('[*].ifalias | [0]', json.loads(j)) or '',
+ 'format': lambda j: _jmes_search('[*].ifalias | [0]', json.loads(j)) or '',
},
'ifindex': {
'shellcmd': 'ip -json -detail link list dev {ifname}',
- 'format': lambda j: jmespath.search('[*].ifindex | [0]', json.loads(j)) or '',
+ 'format': lambda j: _jmes_search('[*].ifindex | [0]', json.loads(j)) or '',
},
'mac': {
'shellcmd': 'ip -json -detail link list dev {ifname}',
- 'format': lambda j: jmespath.search('[*].address | [0]', json.loads(j)),
+ 'format': lambda j: _jmes_search('[*].address | [0]', json.loads(j)),
},
'min_mtu': {
'shellcmd': 'ip -json -detail link list dev {ifname}',
- 'format': lambda j: jmespath.search('[*].min_mtu | [0]', json.loads(j)),
+ 'format': lambda j: _jmes_search('[*].min_mtu | [0]', json.loads(j)),
},
'max_mtu': {
'shellcmd': 'ip -json -detail link list dev {ifname}',
- 'format': lambda j: jmespath.search('[*].max_mtu | [0]', json.loads(j)),
+ 'format': lambda j: _jmes_search('[*].max_mtu | [0]', json.loads(j)),
},
'mtu': {
'shellcmd': 'ip -json -detail link list dev {ifname}',
- 'format': lambda j: jmespath.search('[*].mtu | [0]', json.loads(j)),
+ 'format': lambda j: _jmes_search('[*].mtu | [0]', json.loads(j)),
},
'oper_state': {
'shellcmd': 'ip -json -detail link list dev {ifname}',
- 'format': lambda j: jmespath.search('[*].operstate | [0]', json.loads(j)),
+ 'format': lambda j: _jmes_search('[*].operstate | [0]', json.loads(j)),
},
'vrf': {
'shellcmd': 'ip -json -detail link list dev {ifname}',
- 'format': lambda j: jmespath.search('[?linkinfo.info_slave_kind == `vrf`].master | [0]', json.loads(j)),
+ 'format': lambda j: _jmes_search('[?linkinfo.info_slave_kind == `vrf`].master | [0]', json.loads(j)),
},
}
@@ -552,6 +557,11 @@ class Interface(Control):
sha.update(cpu_id.encode())
sha.update(first_mac.encode())
sha.update(self.ifname.encode())
+ # Imported here rather than at module scope: netaddr is only needed
+ # by this one method.
+ from netaddr import EUI
+ from netaddr import mac_unix_expanded
+
# take the most significant 48 bits from the SHA256 string
tmp = sha.hexdigest()[:12]
# Convert pseudo random string into EUI format which now represents a
@@ -1709,13 +1719,22 @@ class Interface(Control):
def set_eapol(self) -> None:
""" Take care about EAPoL supplicant daemon """
-
# XXX: wpa_supplicant works on the source interface
cfg_dir = '/run/wpa_supplicant'
wpa_supplicant_conf = f'{cfg_dir}/{self.ifname}.conf'
eapol_action='stop'
if 'eapol' in self.config:
+ # Imported here rather than at module scope: vyos.pki imports the
+ # whole cryptography stack, which every consumer of vyos.ifconfig
+ # would otherwise pay for on import. set_eapol() itself runs on
+ # every ethernet/bond update, so keep it inside this guard too.
+ # vyos/config.py and vyos/configverify.py defer vyos.pki the same way.
+ from vyos.pki import find_chain
+ from vyos.pki import encode_certificate
+ from vyos.pki import load_certificate
+ from vyos.pki import wrap_private_key
+
# The default is a fallback to hw_id which is not present for any interface
# other then an ethernet interface. Thus we emulate hw_id by reading back the
# Kernel assigned MAC address
diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py
index caca40b3d..f513b9157 100644
--- a/python/vyos/ifconfig/operational.py
+++ b/python/vyos/ifconfig/operational.py
@@ -18,7 +18,6 @@ import os
from time import time
from datetime import datetime
from functools import reduce
-from tabulate import tabulate
from vyos.ifconfig import Control
@@ -169,6 +168,8 @@ class Operational(Control):
tabs.append([f'{rtx.upper()}:', ] + [_ for _ in self._stat_names[rtx]])
tabs.append(['', ] + [stats[_] for _ in self._stats_dir[rtx]])
+ from tabulate import tabulate
+
s = tabulate(
tabs,
stralign="right",
diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py
index 4949fe571..8c0c473d0 100644
--- a/python/vyos/ifconfig/vrrp.py
+++ b/python/vyos/ifconfig/vrrp.py
@@ -18,7 +18,6 @@ import json
import signal
from time import time
-from tabulate import tabulate
from vyos.configquery import ConfigTreeQuery
from vyos.utils.convert import seconds_to_human
@@ -166,4 +165,6 @@ class VRRP(object):
# add to the active list disabled instances
groups.extend(cls.disabled())
+ from tabulate import tabulate
+
return tabulate(groups, headers)
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index 4609aff25..8801ff43c 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -19,11 +19,7 @@ import time
from datetime import timedelta
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
@@ -84,6 +80,11 @@ class WireGuardOperational(Operational):
return output
def show_interface(self):
+ # Imported here rather than at module scope: display-only helpers should
+ # not be on the vyos.ifconfig import path.
+ from hurry.filesize import size
+ from hurry.filesize import alternative
+
from vyos.config import Config
c = Config()
@@ -183,6 +184,10 @@ class WireGuardOperational(Operational):
return output
def reset_peer(self, peer_name=None, public_key=None):
+ # Imported here rather than at module scope: vyos.configquery pulls in
+ # the whole vyos.config tree.
+ from vyos.configquery import ConfigTreeQuery
+
c = ConfigTreeQuery()
tmp = c.get_config_dict(['interfaces', 'wireguard', self.ifname],
effective=True, get_first_key=True,
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index 421054f6e..6c3158ee6 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -17,8 +17,6 @@ import re
import sys
import typing
-from humps import decamelize
-from vyos.configquery import ConfigTreeQuery
class Error(Exception):
"""Any error that makes requested operation impossible to complete
@@ -319,9 +317,11 @@ def run(module):
f'Bare literal is not an acceptable raw output, must be a list or an object.\
The output was:{res}'
)
+ from humps import decamelize
+ from json import dumps
+
res = decamelize(res)
res = _normalize_field_names(res)
- from json import dumps
return dumps(res, indent=4)
else:
@@ -336,6 +336,11 @@ def verify_cli_exists(cli_path: list, error_msg: str=''):
@wraps(func)
def _wrapper(*args, **kwargs):
+ # Imported here rather than at module scope: vyos.configquery pulls
+ # in the whole vyos.config tree, and most op-mode scripts import
+ # vyos.opmode without ever using this decorator.
+ from vyos.configquery import ConfigTreeQuery
+
config = ConfigTreeQuery()
interface = kwargs.get('intf_name')
diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py
index 65aeb552e..651d3f1e1 100644
--- a/python/vyos/qos/base.py
+++ b/python/vyos/qos/base.py
@@ -14,10 +14,8 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
-import jmespath
from vyos.base import Warning
-from vyos.ifconfig import Interface
from vyos.utils.process import cmdl
from vyos.utils.dict import dict_search
from vyos.utils.file import read_file
@@ -233,6 +231,13 @@ class QoSBase:
def update(self, config, direction, priority=None):
""" method must be called from derived class after it has completed qdisc setup """
+ # Imported here rather than at module scope: pulling vyos.ifconfig in
+ # at import time is what made vyos.qos one of the most expensive
+ # modules in the tree.
+ import jmespath
+
+ from vyos.ifconfig import Interface
+
if self._debug:
import pprint
pprint.pprint(config)
diff --git a/python/vyos/referencetree.py b/python/vyos/referencetree.py
index b535615dd..c2cb654b4 100644
--- a/python/vyos/referencetree.py
+++ b/python/vyos/referencetree.py
@@ -13,12 +13,11 @@
# 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/>.
-from ctypes import cdll, c_char_p, c_void_p, c_bool
from pathlib import Path
from vyos.defaults import reference_tree_cache
-
-LIBPATH = '/usr/lib/libvyosconfig.so.0'
+from vyos.configtree import LIBPATH
+from vyos.configtree import get_lib
class ReferenceTreeError(Exception):
@@ -29,49 +28,26 @@ class ReferenceTree:
# pylint: disable=too-many-instance-attributes,raise-missing-from
def __init__(self, cache_file=reference_tree_cache, libpath=LIBPATH):
self.__pointer = None
- self.__lib = cdll.LoadLibrary(libpath)
-
- # Import functions
- self.__get_error = self.__lib.get_error
- self.__get_error.argtypes = []
- self.__get_error.restype = c_char_p
-
- self.__read_internal_string = self.__lib.read_internal_string_reference_tree
- self.__read_internal_string.argtypes = [c_char_p]
- self.__read_internal_string.restype = c_void_p
-
- self.__write_internal = self.__lib.write_internal_reference_tree
- self.__write_internal.argtypes = [c_void_p, c_char_p]
-
- self.__to_json = self.__lib.to_json_reference_tree
- self.__to_json.argtypes = [c_void_p]
- self.__to_json.restype = c_char_p
-
- self.__destroy = self.__lib.destroy
- self.__destroy.argtypes = [c_void_p]
-
- self.__equal = self.__lib.equal
- self.__equal.argtypes = [c_void_p, c_void_p]
- self.__equal.restype = c_bool
+ self.__lib = get_lib(libpath)
try:
cache_string = Path(cache_file).read_bytes()
except OSError as e:
raise ValueError(f'Failed to read cache_file: {e}')
- pointer = self.__read_internal_string(cache_string)
+ pointer = self.__lib.read_internal_string_reference_tree(cache_string)
if pointer is None:
- msg = self.__get_error().decode()
+ msg = self.__lib.get_error().decode()
raise ValueError(f'Failed to read internal rep: {msg}')
self.__pointer = pointer
def __del__(self):
if self.__pointer is not None:
- self.__destroy(self.__pointer)
+ self.__lib.destroy(self.__pointer)
def __eq__(self, other):
if isinstance(other, ReferenceTree):
- return self.__equal(self.get_tree(), other.get_tree())
+ return self.__lib.equal(self.get_tree(), other.get_tree())
return False
def __str__(self):
@@ -81,7 +57,7 @@ class ReferenceTree:
return self.__pointer
def write_cache(self, file_name):
- self.__write_internal(self.get_tree(), file_name.encode())
+ self.__lib.write_internal_reference_tree(self.get_tree(), file_name.encode())
def to_json(self):
- return self.__to_json(self.__pointer).decode()
+ return self.__lib.to_json_reference_tree(self.__pointer).decode()
diff --git a/python/vyos/template.py b/python/vyos/template.py
index dbbc4df84..83805a73c 100755
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -16,9 +16,6 @@
import functools
import os
-from jinja2 import Environment
-from jinja2 import FileSystemLoader
-from jinja2 import ChainableUndefined
from vyos.defaults import directories
from vyos.utils.dict import dict_search_args
from vyos.utils.file import makedir
@@ -41,6 +38,13 @@ _CLEVER_FUNCTIONS = {}
# reuse Environments with identical settings to improve performance
@functools.lru_cache(maxsize=2)
def _get_environment(location=None):
+ # Imported here rather than at module scope: most importers of this module
+ # want only the pure-Python predicates (is_ipv4, is_ipv6, ...) and never
+ # render a template, so they should not pay for jinja2. This function is
+ # lru_cache'd, so the import happens at most once per process.
+ from jinja2 import Environment
+ from jinja2 import FileSystemLoader
+ from jinja2 import ChainableUndefined
from os import getenv
if location is None:
diff --git a/python/vyos/utils/__init__.py b/python/vyos/utils/__init__.py
index 280cde17f..2d934b75f 100644
--- a/python/vyos/utils/__init__.py
+++ b/python/vyos/utils/__init__.py
@@ -13,21 +13,43 @@
# 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/>.
-from vyos.utils import assertion
-from vyos.utils import auth
-from vyos.utils import boot
-from vyos.utils import commit
-from vyos.utils import configfs
-from vyos.utils import convert
-from vyos.utils import cpu
-from vyos.utils import dict
-from vyos.utils import file
-from vyos.utils import io
-from vyos.utils import kernel
-from vyos.utils import list
-from vyos.utils import locking
-from vyos.utils import misc
-from vyos.utils import network
-from vyos.utils import permission
-from vyos.utils import process
-from vyos.utils import system
+import importlib
+
+__all__ = [
+ 'assertion',
+ 'auth',
+ 'boot',
+ 'commit',
+ 'configfs',
+ 'convert',
+ 'cpu',
+ 'dict',
+ 'file',
+ 'io',
+ 'kernel',
+ 'list',
+ 'locking',
+ 'misc',
+ 'network',
+ 'permission',
+ 'process',
+ 'system',
+]
+
+
+def __getattr__(name):
+ """Import submodules on first access (PEP 562).
+
+ Importing them eagerly pulled the whole set into every consumer of
+ 'from vyos.utils.<sub> import <name>', which is a measurable cost on the
+ conf-mode script path where each script is a fresh interpreter.
+ """
+ if name not in __all__:
+ raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
+ mod = importlib.import_module(f'{__name__}.{name}')
+ globals()[name] = mod
+ return mod
+
+
+def __dir__():
+ return sorted(list(globals()) + __all__)
diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py
index e85dc9984..3950de649 100644
--- a/python/vyos/utils/auth.py
+++ b/python/vyos/utils/auth.py
@@ -13,7 +13,6 @@
# You should have received a copy of the GNU Lesser General Public License along with this library;
# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-import cracklib
import math
import re
import string
@@ -76,15 +75,21 @@ def calculate_entropy(charset: str, passwd: str) -> float:
def evaluate_strength(passwd: str) -> dict[str, str]:
""" Evaluates password strength and returns a check result dict """
- charset = (cracklib.ASCII_UPPERCASE + cracklib.ASCII_LOWERCASE +
- string.punctuation + string.digits)
-
result = {
'strength': '',
'error': '',
}
try:
+ # Imported here rather than at module scope: importing cracklib opens
+ # its dictionary database, so this module stays importable without it.
+ # Kept inside the try so that a missing or broken package is reported
+ # as EPasswdStrength.ERROR instead of escaping this function.
+ import cracklib
+
+ charset = (cracklib.ASCII_UPPERCASE + cracklib.ASCII_LOWERCASE +
+ string.punctuation + string.digits)
+
cracklib.FascistCheck(passwd)
except ValueError as e:
# The password is vulnerable to dictionary attack no matter the entropy
diff --git a/python/vyos/version.py b/python/vyos/version.py
index 4ba44f166..4288a26ff 100644
--- a/python/vyos/version.py
+++ b/python/vyos/version.py
@@ -31,10 +31,7 @@ Example of the version data dict::
import os
-import requests
import vyos.defaults
-from vyos.system.image import is_live_boot
-from urllib3.util import retry
from vyos.utils.file import read_file
from vyos.utils.file import read_json
@@ -70,6 +67,11 @@ def get_version(fname=version_file):
def get_full_version_data(fname=version_file):
+ # Imported here rather than at module scope: vyos.system.image pulls in
+ # vyos.system.grub -> vyos.template -> jinja2, which every consumer of
+ # vyos.version would otherwise pay for on import.
+ from vyos.system.image import is_live_boot
+
version_data = get_version_data(fname)
# Get system architecture (well, kernel architecture rather)
@@ -126,6 +128,9 @@ def get_remote_version(url):
]
"""
+ import requests
+ from urllib3.util import retry
+
headers = {}
session = requests.Session()
diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py
index 91e51338c..932c87964 100644
--- a/python/vyos/vyconf_session.py
+++ b/python/vyos/vyconf_session.py
@@ -31,7 +31,6 @@ 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
from vyos.utils.commit import call_commit_hooks
-from vyos.remote import get_config_file
class VyconfSessionError(Exception):
@@ -246,6 +245,11 @@ class VyconfSession:
self, file_name: str, migrate: bool = False, cached: bool = False
) -> tuple[str, int]:
# pylint: disable=consider-using-with
+ # Imported here rather than at module scope: vyos.remote pulls in
+ # paramiko and requests, which every consumer of vyos.config would
+ # otherwise pay for on import.
+ from vyos.remote import get_config_file
+
file_path = tempfile.NamedTemporaryFile(delete=False).name
err = get_config_file(file_name, file_path)
if err:
@@ -275,6 +279,11 @@ class VyconfSession:
self, file_name: str, migrate: bool = False, destructive: bool = False
) -> tuple[str, int]:
# pylint: disable=consider-using-with
+ # Imported here rather than at module scope: vyos.remote pulls in
+ # paramiko and requests, which every consumer of vyos.config would
+ # otherwise pay for on import.
+ from vyos.remote import get_config_file
+
file_path = tempfile.NamedTemporaryFile(delete=False).name
err = get_config_file(file_name, file_path)
if err: