diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/config.py | 57 | ||||
-rw-r--r-- | python/vyos/configdict.py | 6 | ||||
-rw-r--r-- | python/vyos/configdiff.py | 84 | ||||
-rw-r--r-- | python/vyos/configtree.py | 96 |
4 files changed, 214 insertions, 29 deletions
diff --git a/python/vyos/config.py b/python/vyos/config.py index a5c1ad122..287fd2ed1 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -142,31 +142,41 @@ class Config(object): def exists(self, path): """ - Checks if a node with given path exists in the running or proposed config + Checks if a node or value with given path exists in the proposed config. + + Args: + path (str): Configuration tree path Returns: - True if node exists, False otherwise + True if node or value exists in the proposed config, False otherwise Note: - This function cannot be used outside a configuration sessions. + This function should not be used outside of configuration sessions. In operational mode scripts, use ``exists_effective``. """ - if not self._session_config: + if self._session_config is None: return False + + # Assume the path is a node path first if self._session_config.exists(self._make_path(path)): return True else: + # If that check fails, it may mean the path has a value at the end. # libvyosconfig exists() works only for _nodes_, not _values_ - # libvyattacfg one also worked for values, so we emulate that case here + # libvyattacfg also worked for values, so we emulate that case here if isinstance(path, str): path = re.split(r'\s+', path) path_without_value = path[:-1] - path_str = " ".join(path_without_value) try: - value = self._session_config.return_value(self._make_path(path_str)) - return (value == path[-1]) + # return_values() is safe to use with single-value nodes, + # it simply returns a single-item list in that case. + values = self._session_config.return_values(self._make_path(path_without_value)) + + # If we got this far, the node does exist and has values, + # so we need to check if it has the value in question among its values. + return (path[-1] in values) except vyos.configtree.ConfigTreeError: - # node doesn't exist at all + # Even the parent node doesn't exist at all return False def session_changed(self): @@ -380,7 +390,7 @@ class Config(object): def exists_effective(self, path): """ - Check if a node exists in the running (effective) config + Checks if a node or value exists in the running (effective) config. Args: path (str): Configuration tree path @@ -392,10 +402,31 @@ class Config(object): This function is safe to use in operational mode. In configuration mode, it ignores uncommited changes. """ - if self._running_config: - return(self._running_config.exists(self._make_path(path))) + if self._running_config is None: + return False + + # Assume the path is a node path first + if self._running_config.exists(self._make_path(path)): + return True + else: + # If that check fails, it may mean the path has a value at the end. + # libvyosconfig exists() works only for _nodes_, not _values_ + # libvyattacfg also worked for values, so we emulate that case here + if isinstance(path, str): + path = re.split(r'\s+', path) + path_without_value = path[:-1] + try: + # return_values() is safe to use with single-value nodes, + # it simply returns a single-item list in that case. + values = self._running_config.return_values(self._make_path(path_without_value)) + + # If we got this far, the node does exist and has values, + # so we need to check if it has the value in question among its values. + return (path[-1] in values) + except vyos.configtree.ConfigTreeError: + # Even the parent node doesn't exist at all + return False - return False def return_effective_value(self, path, default=None): """ diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 1f245f3d2..be10cbdfc 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -104,6 +104,12 @@ def list_diff(first, second): second = set(second) return [item for item in first if item not in second] +def is_node_changed(conf, path): + from vyos.configdiff import get_config_diff + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + return D.is_node_changed(path) + def leaf_node_changed(conf, path): """ Check if a leaf node was altered. If it has been altered - values has been diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py index 0e41fbe27..81932e6d0 100644 --- a/python/vyos/configdiff.py +++ b/python/vyos/configdiff.py @@ -16,6 +16,7 @@ from enum import IntFlag, auto from vyos.config import Config +from vyos.configtree import DiffTree from vyos.configdict import dict_merge from vyos.util import get_sub_dict, mangle_dict_keys from vyos.xml import defaults @@ -36,6 +37,8 @@ class Diff(IntFlag): ADD = auto() STABLE = auto() +ALL = Diff.MERGE | Diff.DELETE | Diff.ADD | Diff.STABLE + requires_effective = [enum_to_key(Diff.DELETE)] target_defaults = [enum_to_key(Diff.MERGE)] @@ -73,19 +76,24 @@ def get_config_diff(config, key_mangling=None): isinstance(key_mangling[1], str)): raise ValueError("key_mangling must be a tuple of two strings") - return ConfigDiff(config, key_mangling) + diff_t = DiffTree(config._running_config, config._session_config) + + return ConfigDiff(config, key_mangling, diff_tree=diff_t) class ConfigDiff(object): """ The class of config changes as represented by comparison between the session config dict and the effective config dict. """ - def __init__(self, config, key_mangling=None): + def __init__(self, config, key_mangling=None, diff_tree=None): self._level = config.get_level() self._session_config_dict = config.get_cached_root_dict(effective=False) self._effective_config_dict = config.get_cached_root_dict(effective=True) self._key_mangling = key_mangling + self._diff_tree = diff_tree + self._diff_dict = diff_tree.dict if diff_tree else {} + # mirrored from Config; allow path arguments relative to level def _make_path(self, path): if isinstance(path, str): @@ -134,7 +142,17 @@ class ConfigDiff(object): self._key_mangling[1]) return config_dict - def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + def is_node_changed(self, path=[]): + if self._diff_tree is None: + raise NotImplementedError("diff_tree class not available") + + if (self._diff_tree.add.exists(self._make_path(path)) or + self._diff_tree.sub.exists(self._make_path(path))): + return True + return False + + def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False, + recursive=False): """ Args: path (str|list): config path @@ -144,6 +162,8 @@ class ConfigDiff(object): value no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default values to ret['merge'] + recursive: if true, use config_tree diff algorithm provided by + diff_tree class Returns: dict of lists, representing differences between session and effective config, under path @@ -154,6 +174,34 @@ class ConfigDiff(object): """ session_dict = get_sub_dict(self._session_config_dict, self._make_path(path), get_first_key=True) + + if recursive: + if self._diff_tree is None: + raise NotImplementedError("diff_tree class not available") + else: + add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True) + sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True) + inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True) + ret = {} + ret[enum_to_key(Diff.MERGE)] = session_dict + ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path), + get_first_key=True) + ret[enum_to_key(Diff.ADD)] = get_sub_dict(add, self._make_path(path), + get_first_key=True) + ret[enum_to_key(Diff.STABLE)] = get_sub_dict(inter, self._make_path(path), + get_first_key=True) + for e in Diff: + k = enum_to_key(e) + if not (e & expand_nodes): + ret[k] = list(ret[k]) + else: + if self._key_mangling: + ret[k] = self._mangle_dict_keys(ret[k]) + if k in target_defaults and not no_defaults: + default_values = defaults(self._make_path(path)) + ret[k] = dict_merge(default_values, ret[k]) + return ret + effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path), get_first_key=True) @@ -179,7 +227,8 @@ class ConfigDiff(object): return ret - def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False, + recursive=False): """ Args: path (str|list): config path @@ -189,6 +238,8 @@ class ConfigDiff(object): value no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default values to ret['merge'] + recursive: if true, use config_tree diff algorithm provided by + diff_tree class Returns: dict of lists, representing differences between session and effective config, at path @@ -198,6 +249,31 @@ class ConfigDiff(object): dict['stable'] = config values in both session and effective """ session_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + + if recursive: + if self._diff_tree is None: + raise NotImplementedError("diff_tree class not available") + else: + add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True) + sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True) + inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True) + ret = {} + ret[enum_to_key(Diff.MERGE)] = session_dict + ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path)) + ret[enum_to_key(Diff.ADD)] = get_sub_dict(add, self._make_path(path)) + ret[enum_to_key(Diff.STABLE)] = get_sub_dict(inter, self._make_path(path)) + for e in Diff: + k = enum_to_key(e) + if not (e & expand_nodes): + ret[k] = list(ret[k]) + else: + if self._key_mangling: + ret[k] = self._mangle_dict_keys(ret[k]) + if k in target_defaults and not no_defaults: + default_values = defaults(self._make_path(path)) + ret[k] = dict_merge(default_values, ret[k]) + return ret + effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) ret = _key_sets_from_dicts(session_dict, effective_dict) diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index d8ffaca99..e9cdb69e4 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -17,6 +17,7 @@ import json from ctypes import cdll, c_char_p, c_void_p, c_int +LIBPATH = '/usr/lib/libvyosconfig.so.0' def escape_backslash(string: str) -> str: """Escape single backslashes in string that are not in escape sequence""" @@ -42,7 +43,9 @@ class ConfigTreeError(Exception): class ConfigTree(object): - def __init__(self, config_string, libpath='/usr/lib/libvyosconfig.so.0'): + def __init__(self, config_string=None, address=None, libpath=LIBPATH): + if config_string is None and address is None: + raise TypeError("ConfigTree() requires one of 'config_string' or 'address'") self.__config = None self.__lib = cdll.LoadLibrary(libpath) @@ -60,7 +63,7 @@ class ConfigTree(object): self.__to_string.restype = c_char_p self.__to_commands = self.__lib.to_commands - self.__to_commands.argtypes = [c_void_p] + self.__to_commands.argtypes = [c_void_p, c_char_p] self.__to_commands.restype = c_char_p self.__to_json = self.__lib.to_json @@ -123,18 +126,26 @@ class ConfigTree(object): self.__set_tag.argtypes = [c_void_p, c_char_p] self.__set_tag.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] - config_section, version_section = extract_version(config_string) - config_section = escape_backslash(config_section) - 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)) + if address is None: + config_section, version_section = extract_version(config_string) + config_section = escape_backslash(config_section) + 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)) + else: + self.__config = config + self.__version = version_section else: - self.__config = config - self.__version = version_section + self.__config = address + self.__version = '' def __del__(self): if self.__config is not None: @@ -143,13 +154,16 @@ class ConfigTree(object): def __str__(self): return self.to_string() + def _get_config(self): + return self.__config + def to_string(self): config_string = self.__to_string(self.__config).decode() config_string = "{0}\n{1}".format(config_string, self.__version) return config_string - def to_commands(self): - return self.__to_commands(self.__config).decode() + def to_commands(self, op="set"): + return self.__to_commands(self.__config, op.encode()).decode() def to_json(self): return self.__to_json(self.__config).decode() @@ -281,3 +295,61 @@ class ConfigTree(object): else: raise ConfigTreeError("Path [{}] doesn't exist".format(path_str)) + 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) + subt = ConfigTree(address=res) + return subt + +class DiffTree: + def __init__(self, left, right, path=[], libpath=LIBPATH): + if left is None: + left = ConfigTree(config_string='\n') + if right is None: + right = ConfigTree(config_string='\n') + if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)): + raise TypeError("Arguments must be instances of ConfigTree") + if path: + if not left.exists(path): + raise ConfigTreeError(f"Path {path} doesn't exist in lhs tree") + if not right.exists(path): + raise ConfigTreeError(f"Path {path} doesn't exist in rhs tree") + + 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.__trim_tree = self.__lib.trim_tree + self.__trim_tree.argtypes = [c_void_p, c_void_p] + self.__trim_tree.restype = c_void_p + + check_path(path) + path_str = " ".join(map(str, path)).encode() + + res = self.__diff_tree(path_str, left._get_config(), right._get_config()) + + # full diff config_tree and python dict representation + self.full = ConfigTree(address=res) + self.dict = json.loads(self.full.to_json()) + + # config_tree sub-trees + self.add = self.full.get_subtree(['add']) + self.sub = self.full.get_subtree(['sub']) + self.inter = self.full.get_subtree(['inter']) + + # trim sub(-tract) tree to get delete tree for commands + ref = self.right.get_subtree(path, with_node=True) if path else self.right + res = self.__trim_tree(self.sub._get_config(), ref._get_config()) + self.delete = ConfigTree(address=res) + + def to_commands(self): + add = self.add.to_commands() + delete = self.delete.to_commands(op="delete") + return delete + "\n" + add |