From 0aaf48dcade406b2cdd5c663609eb4d23862cbdf Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 15 Sep 2026 13:36:20 -0500 Subject: T7886: T9231: add missing conversion in get_owner --- python/vyos/referencetree.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'python') diff --git a/python/vyos/referencetree.py b/python/vyos/referencetree.py index 0cd969762..24e8da1ec 100644 --- a/python/vyos/referencetree.py +++ b/python/vyos/referencetree.py @@ -71,7 +71,14 @@ class ReferenceTree: return bool(res) def get_owner(self, path): - return self.__lib.get_owner(self.__pointer, path.encode()).decode() + check_path(path) + path_str = ' '.join(map(str, path)).encode() + + owner = self.__lib.get_owner(self.__pointer, path_str) + if not owner: + return None + + return owner.decode() def get_multi_nodes(self, tag_value_placeholder='', as_tuple=False): import json -- cgit v1.2.3 From 0d3e730c9e71748f866573a72951e142b4108a7b Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 24 Sep 2026 12:01:15 -0500 Subject: T7886: add supporting functions for calculation of reverse dependencies --- python/vyos/dependentverify.py | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 python/vyos/dependentverify.py (limited to 'python') diff --git a/python/vyos/dependentverify.py b/python/vyos/dependentverify.py new file mode 100644 index 000000000..157f99ae3 --- /dev/null +++ b/python/vyos/dependentverify.py @@ -0,0 +1,62 @@ +# 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.configtree import ConfigTree # noqa: I001 +from vyos.configtree import DiffTree +from vyos.configtree import subtree_values_of_path +from vyos.referencetree import ReferenceTree + + +def removed_dependency_value_paths_of_kind( + kind: str, + reference_tree: ReferenceTree, + session_tree: ConfigTree, + diff_tree: DiffTree, +): + paths = reference_tree.get_nodes_of_kind(kind) + value_paths = [] + for path in paths: + path_vals = subtree_values_of_path(diff_tree.sub, path, reference_tree) + path_vals = [t for t in path_vals if t != ([], [])] + for vlst, p in path_vals: + vals = list( + filter( + lambda v, cur_p=p: not session_tree.exists(cur_p + [v]), + vlst, + ) + ) + if vals: + value_paths.append((vals, p)) + + return value_paths + + +def dependent_value_paths_of_kind( + kind: str, + reference_tree: ReferenceTree, + session_tree: ConfigTree, +): + dependent_data = reference_tree.get_rdeps_of_kind_data(kind) + + dependent_value_paths = {} + for p, a in dependent_data: + path_vals = subtree_values_of_path(session_tree, p, reference_tree) + path_vals = [t for t in path_vals if t != ([], [])] + if path_vals: + lst = dependent_value_paths.setdefault(a, []) + lst.extend(path_vals) + + return dependent_value_paths -- cgit v1.2.3 From ec64edcb5b014125dfd5574f40704423b0c0831b Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 15 Sep 2026 13:37:55 -0500 Subject: T7886: add verification of reverse dependencies Suggested-by: l0crian1 --- python/vyos/configmanager.py | 116 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) (limited to 'python') diff --git a/python/vyos/configmanager.py b/python/vyos/configmanager.py index 46bff08b0..895d43ba7 100644 --- a/python/vyos/configmanager.py +++ b/python/vyos/configmanager.py @@ -13,7 +13,9 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . -import json + +import json # noqa: I001 +import logging from types import ModuleType from dataclasses import dataclass @@ -23,9 +25,25 @@ from pathlib import Path from vyos.config import Config from vyos.config import ConfigDict from vyos.configtree import ConfigTree +from vyos.configtree import DiffTree from vyos.referencetree import ReferenceTree +from vyos.dependentverify import removed_dependency_value_paths_of_kind +from vyos.dependentverify import dependent_value_paths_of_kind + from vyos.utils.system import load_as_module from vyos.defaults import directories +from vyos.base import Warning as Warn +from vyos import ConfigError + +LOG = logging.getLogger(Path(__file__).stem) +handler = logging.StreamHandler() +formatter = logging.Formatter('(%(name)s): %(message)s') +handler.setFormatter(formatter) +LOG.addHandler(handler) + +debug = False + +_ = LOG.setLevel(logging.DEBUG) if debug else LOG.setLevel(logging.INFO) @dataclass @@ -38,6 +56,8 @@ class ConfigDictCache: class Component: module: ModuleType name: str + rdep_error: list = field(default_factory=list) + rdep_warning: list = field(default_factory=list) cache: ConfigDictCache = None @@ -54,6 +74,7 @@ class ConfigManager: self.config: Config = None self.running_config: ConfigTree = None self.session_config: ConfigTree = None + self.diff_tree: DiffTree = None self.components = self.init_components() @@ -92,14 +113,100 @@ class ConfigManager: if config is None: self.running_config = None self.session_config = None + self.diff_tree = None else: self.running_config = config.get_config_tree(effective=True) self.session_config = config.get_config_tree() - setattr(config, 'manager', self) + self.diff_tree = DiffTree(self.running_config, self.session_config) if self.cache_config: self.clear_cache() + self.clear_rdep() + + if config is None: + return + + self.init_rdep() + + def init_rdep(self): + # pylint: disable=too-many-locals,too-many-branches + """ + Schema definition for reverse dependency alert: + alertAttr = attribute alert + { + "error" | "warning" + } + + We are currently only considering reverse dependencies for type + (kind) 'interface'. Extension for other kinds may be added here. + + The reference tree utils return data of the form outlined below. In + all cases, the input path is a reference tree path, and consequently + may define a subtree of the config (due to intermediary tag node); + thus the return values are lists of (tuples including) actual config + paths. + + removed_dependency_value_paths: list[tuple[list(values), path]] + are the values for each path defined by the XML paths of kind + 'interface', which are to be removed in the current commit session. + + dependent_value_paths: dict[alert: list[tuple[list(values), path]]] + are the (values, path) defined by all XML dependency annotations + existing in the session tree, indexed by alert type. + + The dependent_value_paths are cross-referenced with the interface + values scheduled for deletion in removed_dependency_value_paths, and + the corresponding alert raised in the configmanager verify stage. + """ + removed_dependency_value_paths = removed_dependency_value_paths_of_kind( + 'interface', self.reference_tree, self.session_config, self.diff_tree + ) + + LOG.debug(f'removed_dependency_value_paths: {removed_dependency_value_paths}') + + if not removed_dependency_value_paths: + return + + dependent_value_paths = dependent_value_paths_of_kind( + 'interface', self.reference_tree, self.session_config + ) + + LOG.debug(f'dependent_value_paths: {dependent_value_paths}') + + if not dependent_value_paths: + return + + for vals, path in removed_dependency_value_paths: + owner = self.reference_tree.get_owner(path) + if owner is None: + LOG.error(f'Path {path} has no owner') + continue + component_name = Path(owner).stem + component = self.components.get(component_name) + LOG.debug(f'owner: {component_name}') + + errors = dependent_value_paths.get('error', []) + rdep_err = component.rdep_error + for dependent_vals, dependent_path in errors: + for v in set(vals) & set(dependent_vals): + rdep_err.append( + f'\nNode:\n {path + [v]}\nis required by:\n {dependent_path}\n' + ) + + warnings = dependent_value_paths.get('warning', []) + rdep_warn = component.rdep_warning + for dependent_vals, dependent_path in warnings: + for v in set(vals) & set(dependent_vals): + rdep_warn.append( + f'removed node:\n {path + [v]}\nis referenced by:\n {dependent_path}\n' + ) + + def clear_rdep(self): + for component in self.components.values(): + component.rdep_error = [] + component.rdep_warning = [] + def clear_cache(self): for component in self.components.values(): component.cache = None @@ -121,6 +228,11 @@ class ConfigManager: component = self.components[script_name] mod = component.module + if errors := component.rdep_error: + raise ConfigError('\n'.join(errors)) + if warnings := component.rdep_warning: + Warn('\n'.join(warnings)) + mod.verify(config_dict) def generate(self, script_name: str, config_dict: ConfigDict) -> None: -- cgit v1.2.3 From 0b43a668ac1fdbc5149616f5065279bbce9a02c8 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 6 Oct 2026 11:50:21 -0500 Subject: T7886: add concatenation of vif entries --- .../include/interface/vif-s.xml.i | 2 + interface-definitions/include/interface/vif.xml.i | 1 + python/vyos/configmanager.py | 18 ++++++-- python/vyos/dependentverify.py | 53 ++++++++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) (limited to 'python') diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i index 65ca10207..e07a9bf8a 100644 --- a/interface-definitions/include/interface/vif-s.xml.i +++ b/interface-definitions/include/interface/vif-s.xml.i @@ -10,6 +10,7 @@ VLAN ID must be between 0 and 4094 + interface #include @@ -32,6 +33,7 @@ VLAN ID must be between 0 and 4094 + interface #include diff --git a/interface-definitions/include/interface/vif.xml.i b/interface-definitions/include/interface/vif.xml.i index 87f91c5ce..e9b9385f1 100644 --- a/interface-definitions/include/interface/vif.xml.i +++ b/interface-definitions/include/interface/vif.xml.i @@ -10,6 +10,7 @@ VLAN ID must be between 0 and 4094 + interface #include diff --git a/python/vyos/configmanager.py b/python/vyos/configmanager.py index 895d43ba7..69d40e5ef 100644 --- a/python/vyos/configmanager.py +++ b/python/vyos/configmanager.py @@ -29,6 +29,7 @@ from vyos.configtree import DiffTree from vyos.referencetree import ReferenceTree from vyos.dependentverify import removed_dependency_value_paths_of_kind from vyos.dependentverify import dependent_value_paths_of_kind +from vyos.dependentverify import concatenate_vif from vyos.utils.system import load_as_module from vyos.defaults import directories @@ -163,6 +164,10 @@ class ConfigManager: 'interface', self.reference_tree, self.session_config, self.diff_tree ) + removed_dependency_value_paths, orig_value = concatenate_vif( + self.reference_tree, removed_dependency_value_paths + ) + LOG.debug(f'removed_dependency_value_paths: {removed_dependency_value_paths}') if not removed_dependency_value_paths: @@ -178,7 +183,8 @@ class ConfigManager: return for vals, path in removed_dependency_value_paths: - owner = self.reference_tree.get_owner(path) + rpath = self.reference_tree.reference_path_from_config_path(path) + owner = self.reference_tree.get_owner(rpath) if owner is None: LOG.error(f'Path {path} has no owner') continue @@ -190,16 +196,22 @@ class ConfigManager: rdep_err = component.rdep_error for dependent_vals, dependent_path in errors: for v in set(vals) & set(dependent_vals): + ov = orig_value.get(v, v) + path_str = ' '.join(path + [ov]) + dpath_str = ' '.join(dependent_path) rdep_err.append( - f'\nNode:\n {path + [v]}\nis required by:\n {dependent_path}\n' + f'\nNode:\n "{path_str}"\nis required by:\n "{dpath_str}"\n' ) warnings = dependent_value_paths.get('warning', []) rdep_warn = component.rdep_warning for dependent_vals, dependent_path in warnings: for v in set(vals) & set(dependent_vals): + ov = orig_value.get(v, v) + path_str = ' '.join(path + [ov]) + dpath_str = ' '.join(dependent_path) rdep_warn.append( - f'removed node:\n {path + [v]}\nis referenced by:\n {dependent_path}\n' + f'removed node:\n "{path_str}"\nis referenced by:\n "{dpath_str}"\n' ) def clear_rdep(self): diff --git a/python/vyos/dependentverify.py b/python/vyos/dependentverify.py index 157f99ae3..1e22b65a0 100644 --- a/python/vyos/dependentverify.py +++ b/python/vyos/dependentverify.py @@ -18,6 +18,7 @@ from vyos.configtree import ConfigTree # noqa: I001 from vyos.configtree import DiffTree from vyos.configtree import subtree_values_of_path from vyos.referencetree import ReferenceTree +from vyos.base import Warning as Warn def removed_dependency_value_paths_of_kind( @@ -60,3 +61,55 @@ def dependent_value_paths_of_kind( lst.extend(path_vals) return dependent_value_paths + + +vif_refpath = [ + ['interfaces', 'ethernet', 'vif'], + ['interfaces', 'ethernet', 'vif-s'], + ['interfaces', 'ethernet', 'vif-s', 'vif-c'], + ['interfaces', 'bonding', 'vif'], + ['interfaces', 'bonding', 'vif-s'], + ['interfaces', 'bonding', 'vif-s', 'vif-c'], + ['interfaces', 'bridge', 'vif'], + ['interfaces', 'pseudo-ethernet', 'vif'], + ['interfaces', 'pseudo-ethernet', 'vif-s'], + ['interfaces', 'pseudo-ethernet', 'vif-s', 'vif-c'], + ['interfaces', 'virtual-ethernet', 'vif'], + ['interfaces', 'virtual-ethernet', 'vif-s'], + ['interfaces', 'virtual-ethernet', 'vif-s', 'vif-c'], + ['interfaces', 'wireless', 'vif'], + ['interfaces', 'wireless', 'vif-s'], + ['interfaces', 'wireless', 'vif-s', 'vif-c'], +] + + +def check_vif_path(reference_tree: ReferenceTree): + for p in vif_refpath: + if not reference_tree.exists(p): + Warn(f'Out of date reference path: {p}') + + +def concatenate_vif(reference_tree: ReferenceTree, value_paths): + check_vif_path(reference_tree) + revised = [] + orig_value = {} + for vp in value_paths: + refpath = reference_tree.reference_path_from_config_path(vp[1]) + if refpath not in vif_refpath: + revised.append(vp) + else: + p = vp[1] + match p[-1]: + case 'vif': + rev_vals = [p[2] + '.' + v for v in vp[0]] + orig_value |= dict(zip(rev_vals, vp[0])) + revised.append((rev_vals, vp[1])) + case 'vif-s': + rev_vals = [p[2] + '.' + v for v in vp[0]] + orig_value |= dict(zip(rev_vals, vp[0])) + revised.append((rev_vals, vp[1])) + case 'vif-c': + rev_vals = [p[2] + '.' + p[4] + '.' + v for v in vp[0]] + orig_value |= dict(zip(rev_vals, vp[0])) + revised.append((rev_vals, vp[1])) + return revised, orig_value -- cgit v1.2.3