From 7a49b5bde4cc220977bd5719a1ecd2ffabe03ca9 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 12 Apr 2026 15:09:11 -0500 Subject: T8488: add wrapper for mask_exclusive --- python/vyos/configtree.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) (limited to 'python') diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 61ef6428c..2876130cf 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -584,13 +584,38 @@ def mask_inclusive(left, right, libpath=LIBPATH): try: __lib = cdll.LoadLibrary(libpath) __mask_tree = __lib.mask_tree - __mask_tree.argtypes = [c_void_p, c_void_p] + __mask_tree.argtypes = [c_void_p, c_void_p, c_bool] __mask_tree.restype = c_void_p __get_error = __lib.get_error __get_error.argtypes = [] __get_error.restype = c_char_p - res = __mask_tree(left.get_tree(), right.get_tree()) + res = __mask_tree(left.get_tree(), right.get_tree(), False) + except Exception as e: + raise ConfigTreeError(e) + if not res: + msg = __get_error().decode() + raise ConfigTreeError(msg) + + tree = ConfigTree(address=res) + + return tree + + +def mask_exclusive(left, right, libpath=LIBPATH): + if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)): + raise TypeError('Arguments must be instances of ConfigTree') + + try: + __lib = cdll.LoadLibrary(libpath) + __mask_tree = __lib.mask_tree + __mask_tree.argtypes = [c_void_p, c_void_p, c_bool] + __mask_tree.restype = c_void_p + __get_error = __lib.get_error + __get_error.argtypes = [] + __get_error.restype = c_char_p + + res = __mask_tree(left.get_tree(), right.get_tree(), True) except Exception as e: raise ConfigTreeError(e) if not res: -- cgit v1.2.3 From c02e59b9c655f8b79bfc490a3d1b3527545b99b2 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 12 Apr 2026 16:23:55 -0500 Subject: T8488: add wrapper for subtree_from_partial --- python/vyos/configtree.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) (limited to 'python') diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 2876130cf..f93ec668b 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -18,6 +18,12 @@ import json import logging from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool +from typing import TYPE_CHECKING + +# https://peps.python.org/pep-0484/#forward-references +# for type 'ConfigDict' +if TYPE_CHECKING: + from vyos.referencetree import ReferenceTree BUILD_PATH = '/tmp/libvyosconfig/_build/libvyosconfig.so' INSTALL_PATH = '/usr/lib/libvyosconfig.so.0' @@ -627,6 +633,49 @@ def mask_exclusive(left, right, libpath=LIBPATH): return tree +def subtree_from_partial( + config_tree: ConfigTree, + path: list[str], + reference_tree: 'ReferenceTree', + start: ConfigTree = None, + libpath=LIBPATH, +): + if start: + if not isinstance(start, ConfigTree): + raise TypeError("Argument 'start' must be an instance of ConfigTree") + else: + start = ConfigTree('') + + check_path(path) + 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( + reference_tree.get_tree(), + config_tree.get_tree(), + start.get_tree(), + path_str, + ) + except Exception as e: + raise ConfigTreeError(e) + if not res: + msg = __get_error().decode() + # msg is space separated list of path + raise ConfigTreeError(msg.split()) + + tree = ConfigTree(address=res) + + return tree + + def reference_tree_to_json(from_dir, to_file, internal_cache='', libpath=LIBPATH): try: __lib = cdll.LoadLibrary(libpath) -- cgit v1.2.3 From c4fcf52f027d58987951299dc30b95affe52c3e0 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 13 Apr 2026 13:26:39 -0500 Subject: T8488: add utility to define exclusion mask from list of partial paths A partial path is one that may or may not include intervening tag node values. For unspecified tag node values, the matching subtree is returned. For example ['interfaces', 'ethernet', 'address'] will return the subtree of the config for which ethernet tag values have defined address. --- python/vyos/derivedtree.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 python/vyos/derivedtree.py (limited to 'python') diff --git a/python/vyos/derivedtree.py b/python/vyos/derivedtree.py new file mode 100644 index 000000000..0a57921b2 --- /dev/null +++ b/python/vyos/derivedtree.py @@ -0,0 +1,56 @@ +# Copyright (C) VyOS Inc. +# +# 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 . + + +from vyos.referencetree import ReferenceTree +from vyos.configtree import ConfigTree +from vyos.configtree import ConfigTreeError +from vyos.configtree import subtree_from_partial + + +class DerivedTreeError(Exception): + """Error to be raised by functions of derivedtree""" + + +def subtree_from_list_of_partial_paths( + ctree: ConfigTree, paths: list[list[str]], accumulator: ConfigTree = None +): + """Return the union of subtrees of the ConfigTree argument matching each + of the 'partial' paths. A partial path is one that may or may not + contain intervening tag node values, in which case it will match for all + values that apply. + + An existing subtree may be passed as the initial value of accumulator. + """ + if accumulator: + if not isinstance(accumulator, ConfigTree): + raise TypeError("Argument 'accumulator' must be an instance of ConfigTree") + else: + accumulator = ConfigTree('') + + rtree = ReferenceTree() + + errors = [] + for path in paths: + try: + accumulator = subtree_from_partial(ctree, path, rtree, accumulator) + except ConfigTreeError as e: + errors.append(str(e)) + continue + + if errors: + raise DerivedTreeError(f'Nonsensical paths: {errors}') + + return accumulator -- cgit v1.2.3 From 913715e2d6a5784dd53770119697c469c0c097bc Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 15 Apr 2026 21:18:00 -0500 Subject: T8488: add wrappers for read/write_internal_string --- python/vyos/configtree.py | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) (limited to 'python') diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index f93ec668b..d92137c14 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -73,11 +73,21 @@ class ConfigTreeError(Exception): class ConfigTree(object): def __init__( - self, config_string=None, address=None, internal=None, libpath=LIBPATH + self, + config_string=None, + address=None, + internal=None, + internal_string=None, + libpath=LIBPATH, ): - if config_string is None and address is None and internal is None: + if ( + config_string is None + and address is None + and internal is None + and internal_string is None + ): raise TypeError( - "ConfigTree() requires one of 'config_string', 'address', or 'internal'" + "ConfigTree() requires one of 'config_string', 'address', 'internal', or 'internal_string'" ) self.__config = None @@ -107,6 +117,14 @@ class ConfigTree(object): 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 @@ -212,7 +230,19 @@ class ConfigTree(object): config = self.__read_internal(internal.encode()) if config is None: msg = self.__get_error().decode() - raise ValueError('Failed to read internal rep: {0}'.format(msg)) + raise ValueError( + f'Failed to read internal representation from file {internal}: {msg}' + ) + else: + self.__config = config + self.__version = '' + elif internal_string is not None: + config = self.__read_internal_string(internal_string.encode()) + if config is None: + msg = self.__get_error().decode() + raise ValueError( + f'Failed to read internal representation from string: {msg}' + ) else: self.__config = config self.__version = '' @@ -222,7 +252,7 @@ class ConfigTree(object): config = self.__from_string(config_section.encode()) if config is None: msg = self.__get_error().decode() - raise ValueError('Failed to parse config: {0}'.format(msg)) + raise ValueError(f'Failed to parse config: {msg}') else: self.__config = config self.__version = version_section @@ -256,6 +286,10 @@ class ConfigTree(object): def write_cache(self, file_name): self.__write_internal(self.get_tree(), file_name.encode()) + def write_internal_string(self) -> str: + res = self.__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 = unescape_backslash(config_string) @@ -668,8 +702,7 @@ def subtree_from_partial( raise ConfigTreeError(e) if not res: msg = __get_error().decode() - # msg is space separated list of path - raise ConfigTreeError(msg.split()) + raise ConfigTreeError(msg) tree = ConfigTree(address=res) -- cgit v1.2.3