summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Estabrook <jestabro@vyos.io>2026-05-11 15:50:21 -0500
committerGitHub <noreply@github.com>2026-05-11 15:50:21 -0500
commit3c0548641701e6531e80971845503aa7e9497031 (patch)
tree4d190bf08179392bf001e877685b9d3a8d9778c3
parentf0b930d61b22052bc9ace5545744219746e179c9 (diff)
parentb52d09062b1adf8a45cae1b81e4ae53ec477f3cb (diff)
downloadvyos-1x-3c0548641701e6531e80971845503aa7e9497031.tar.gz
vyos-1x-3c0548641701e6531e80971845503aa7e9497031.zip
Merge pull request #5127 from jestabro/mask-exclusive
T8488: add support for generating and applying an exclude mask to configtree
-rw-r--r--libvyosconfig/Makefile2
-rw-r--r--libvyosconfig/lib/bindings.ml36
-rw-r--r--python/vyos/configtree.py121
-rw-r--r--python/vyos/derivedtree.py56
4 files changed, 204 insertions, 11 deletions
diff --git a/libvyosconfig/Makefile b/libvyosconfig/Makefile
index 8e37495b1..26563ef78 100644
--- a/libvyosconfig/Makefile
+++ b/libvyosconfig/Makefile
@@ -42,7 +42,7 @@ all: sharedlib
PHONY: depends
depends:
sudo sh -c 'eval $$(opam env --root=/opt/opam --set-root) ;\
- opam pin add vyos1x-config https://github.com/vyos/vyos1x-config.git#52132ad2c0992bf6f17a06173384030d93a29053 -y ; \
+ opam pin add vyos1x-config https://github.com/vyos/vyos1x-config.git#e80771d973fc249ab04fd20a6bb2463698194880 -y ; \
opam pin add vyconf https://github.com/vyos/vyconf.git#e25b13ae3040d02326f01bf9bedd097795fb3a62 -y'
sharedlib: depends $(BUILDDIR)/libvyosconfig$(EXTDLL)
diff --git a/libvyosconfig/lib/bindings.ml b/libvyosconfig/lib/bindings.ml
index 5d0478e02..0759edae2 100644
--- a/libvyosconfig/lib/bindings.ml
+++ b/libvyosconfig/lib/bindings.ml
@@ -95,6 +95,21 @@ let write_internal c_ptr file =
with Internal.Write_error msg ->
error_message := msg
+let read_internal_string s =
+ (* alert exn Internal.read_string:
+ [Internal.Read_error] caught
+ *)
+ try
+ error_message := "";
+ let ct = (I.read_string[@alert "-exn"]) s in
+ Ctypes.Root.create ct
+ with Internal.Read_error msg ->
+ error_message := msg; Ctypes.null
+
+let write_internal_string c_ptr =
+ let ct = Root.get c_ptr in
+ I.write_string ct
+
let render_json_reference_tree c_ptr =
RT.render_json (Root.get c_ptr)
@@ -462,7 +477,7 @@ let reference_tree_to_json internal_cache from_dir to_file =
let s = Printf.sprintf "Write_error \'%s\'" msg in
error_message := s; 1
-let mask_tree c_ptr_l c_ptr_r =
+let mask_tree c_ptr_l c_ptr_r exclusive =
(* alert exn CD.mask_tree:
[Config_diff.Incommensurable] caught
[Config_diff.Empty_comparison] caught
@@ -470,12 +485,24 @@ let mask_tree c_ptr_l c_ptr_r =
let ct_l = Root.get c_ptr_l in
let ct_r = Root.get c_ptr_r in
try
- let ct_ret = (CD.mask_tree[@alert "-exn"]) ct_l ct_r in
+ let ct_ret = (CD.mask_tree[@alert "-exn"]) ~exclusive:exclusive ct_l ct_r in
Ctypes.Root.create ct_ret
with
| CD.Incommensurable -> error_message := "Incommensurable"; Ctypes.null
| CD.Empty_comparison -> error_message := "Empty comparison"; Ctypes.null
+let subtree_from_partial r_ptr c_ptr i_ptr path =
+ let rt = Root.get r_ptr in
+ let ct = Root.get c_ptr in
+ let input = Root.get i_ptr in
+ let path = split_on_whitespace path in
+ try
+ let ct_ret = (CD.subtree_from_partial[@alert "-exn"]) rt ct input path in
+ Ctypes.Root.create ct_ret
+ with
+ CD.Malformed_path s ->
+ error_message := s; Ctypes.null
+
let validate_tree_filter c_ptr rt_cache_path validator_dir =
(* alert exn Internal.read_internal:
[Internal.Read_error] caught
@@ -513,6 +540,8 @@ struct
let () = I.internal "to_commands" ((ptr void) @-> string @-> returning string) render_commands
let () = I.internal "read_internal" (string @-> returning (ptr void)) read_internal
let () = I.internal "write_internal" ((ptr void) @-> string @-> returning void) write_internal
+ let () = I.internal "read_internal_string" (string @-> returning (ptr void)) read_internal_string
+ let () = I.internal "write_internal_string" ((ptr void) @-> returning string) write_internal_string
let () = I.internal "to_json_reference_tree" ((ptr void) @-> returning string) render_json_reference_tree
let () = I.internal "read_internal_reference_tree" (string @-> returning (ptr void)) read_internal_reference_tree
let () = I.internal "write_internal_reference_tree" ((ptr void) @-> string @-> returning void) write_internal_reference_tree
@@ -539,7 +568,8 @@ struct
let () = I.internal "tree_union" ((ptr void) @-> (ptr void) @-> returning (ptr void)) tree_union
let () = I.internal "tree_merge" (bool @-> (ptr void) @-> (ptr void) @-> returning (ptr void)) tree_merge
let () = I.internal "reference_tree_to_json" (string @-> string @-> string @-> returning int) reference_tree_to_json
- let () = I.internal "mask_tree" ((ptr void) @-> (ptr void) @-> returning (ptr void)) mask_tree
+ let () = I.internal "mask_tree" ((ptr void) @-> (ptr void) @-> bool @-> returning (ptr void)) mask_tree
+ let () = I.internal "subtree_from_partial" ((ptr void) @-> (ptr void) @-> (ptr void) @-> string @-> returning (ptr void)) subtree_from_partial
let () = I.internal "validate_tree_filter" ((ptr void) @-> string @-> string @-> returning (ptr void)) validate_tree_filter
let () = I.internal "config_dict" ((ptr void) @-> (ptr void) @-> (ptr void) @-> string @-> bool @-> bool @-> returning string) config_dict
end
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 61ef6428c..d92137c14 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'
@@ -67,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
@@ -101,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
@@ -206,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 = ''
@@ -216,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
@@ -250,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)
@@ -584,13 +624,80 @@ 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(), 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())
+ res = __mask_tree(left.get_tree(), right.get_tree(), True)
+ except Exception as e:
+ raise ConfigTreeError(e)
+ if not res:
+ msg = __get_error().decode()
+ raise ConfigTreeError(msg)
+
+ tree = ConfigTree(address=res)
+
+ 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:
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 <http://www.gnu.org/licenses/>.
+
+
+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