summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/configmanager.py128
-rw-r--r--python/vyos/dependentverify.py115
-rw-r--r--python/vyos/referencetree.py9
3 files changed, 249 insertions, 3 deletions
diff --git a/python/vyos/configmanager.py b/python/vyos/configmanager.py
index 46bff08b0..69d40e5ef 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 <http://www.gnu.org/licenses/>.
-import json
+
+import json # noqa: I001
+import logging
from types import ModuleType
from dataclasses import dataclass
@@ -23,9 +25,26 @@ 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.dependentverify import concatenate_vif
+
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 +57,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 +75,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 +114,111 @@ 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
+ )
+
+ 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:
+ 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:
+ 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
+ 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):
+ ov = orig_value.get(v, v)
+ path_str = ' '.join(path + [ov])
+ dpath_str = ' '.join(dependent_path)
+ rdep_err.append(
+ 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_str}"\nis referenced by:\n "{dpath_str}"\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 +240,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:
diff --git a/python/vyos/dependentverify.py b/python/vyos/dependentverify.py
new file mode 100644
index 000000000..1e22b65a0
--- /dev/null
+++ b/python/vyos/dependentverify.py
@@ -0,0 +1,115 @@
+# 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.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(
+ 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
+
+
+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
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