diff options
Diffstat (limited to 'python/vyos')
-rw-r--r-- | python/vyos/config.py | 184 | ||||
-rw-r--r-- | python/vyos/configdict.py | 5 | ||||
-rw-r--r-- | python/vyos/configdiff.py | 249 | ||||
-rw-r--r-- | python/vyos/configsource.py | 318 | ||||
-rw-r--r-- | python/vyos/configverify.py | 78 | ||||
-rw-r--r-- | python/vyos/frr.py | 288 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 52 | ||||
-rw-r--r-- | python/vyos/ifconfig/loopback.py | 25 | ||||
-rw-r--r-- | python/vyos/snmpv3_hashgen.py | 50 | ||||
-rw-r--r-- | python/vyos/template.py | 29 | ||||
-rw-r--r-- | python/vyos/util.py | 49 | ||||
-rw-r--r-- | python/vyos/validate.py | 7 | ||||
-rw-r--r-- | python/vyos/xml/__init__.py | 26 | ||||
-rw-r--r-- | python/vyos/xml/definition.py | 81 | ||||
-rw-r--r-- | python/vyos/xml/kw.py | 12 | ||||
-rw-r--r-- | python/vyos/xml/test_xml.py | 2 |
16 files changed, 1268 insertions, 187 deletions
diff --git a/python/vyos/config.py b/python/vyos/config.py index 56353c322..884d6d947 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -63,23 +63,13 @@ In operational mode, all functions return values from the running config. """ -import os import re import json -import subprocess +from copy import deepcopy import vyos.util import vyos.configtree - - -class VyOSError(Exception): - """ - Raised on config access errors, most commonly if the type of a config tree node - in the system does not match the type of operation. - - """ - pass - +from vyos.configsource import ConfigSource, ConfigSourceSession class Config(object): """ @@ -89,49 +79,18 @@ class Config(object): the only state it keeps is relative *config path* for convenient access to config subtrees. """ - def __init__(self, session_env=None): - self._cli_shell_api = "/bin/cli-shell-api" - self._level = [] - if session_env: - self.__session_env = session_env - else: - self.__session_env = None - - # Running config can be obtained either from op or conf mode, it always succeeds - # once the config system is initialized during boot; - # before initialization, set to empty string - if os.path.isfile('/tmp/vyos-config-status'): - try: - running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) - except VyOSError: - running_config_text = '' - else: - running_config_text = '' - - # Session config ("active") only exists in conf mode. - # In op mode, we'll just use the same running config for both active and session configs. - if self.in_session(): - try: - session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) - except VyOSError: - session_config_text = '' - else: - session_config_text = running_config_text - - if running_config_text: - self._running_config = vyos.configtree.ConfigTree(running_config_text) - else: - self._running_config = None - - if session_config_text: - self._session_config = vyos.configtree.ConfigTree(session_config_text) + def __init__(self, session_env=None, config_source=None): + if config_source is None: + self._config_source = ConfigSourceSession(session_env) else: - self._session_config = None + if not isinstance(config_source, ConfigSource): + raise TypeError("config_source not of type ConfigSource") + self._config_source = config_source - def _make_command(self, op, path): - args = path.split() - cmd = [self._cli_shell_api, op] + args - return cmd + self._level = [] + self._dict_cache = {} + (self._running_config, + self._session_config) = self._config_source.get_configtree_tuple() def _make_path(self, path): # Backwards-compatibility stuff: original implementation used string paths @@ -147,19 +106,6 @@ class Config(object): raise TypeError("Path must be a whitespace-separated string or a list") return (self._level + path) - def _run(self, cmd): - if self.__session_env: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env) - else: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE) - out = p.stdout.read() - p.wait() - p.communicate() - if p.returncode != 0: - raise VyOSError() - else: - return out.decode('ascii') - def set_level(self, path): """ Set the *edit level*, that is, a relative config tree path. @@ -227,22 +173,14 @@ class Config(object): Returns: True if the config session has uncommited changes, False otherwise. """ - try: - self._run(self._make_command('sessionChanged', '')) - return True - except VyOSError: - return False + return self._config_source.session_changed() def in_session(self): """ Returns: True if called from a configuration session, False otherwise. """ - try: - self._run(self._make_command('inSession', '')) - return True - except VyOSError: - return False + return self._config_source.in_session() def show_config(self, path=[], default=None, effective=False): """ @@ -253,52 +191,39 @@ class Config(object): Returns: str: working configuration """ + return self._config_source.show_config(path, default, effective) - # show_config should be independent of CLI edit level. - # Set the CLI edit environment to the top level, and - # restore original on exit. - save_env = self.__session_env + def get_cached_dict(self, effective=False): + cached = self._dict_cache.get(effective, {}) + if cached: + config_dict = cached + else: + config_dict = {} - env_str = self._run(self._make_command('getEditResetEnv', '')) - env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) - root_env = os.environ - for k, v in env_list: - root_env[k] = v + if effective: + if self._running_config: + config_dict = json.loads((self._running_config).to_json()) + else: + if self._session_config: + config_dict = json.loads((self._session_config).to_json()) - self.__session_env = root_env + self._dict_cache[effective] = config_dict - # FIXUP: by default, showConfig will give you a diff - # if there are uncommitted changes. - # The config parser obviously cannot work with diffs, - # so we need to supress diff production using appropriate - # options for getting either running (active) - # or proposed (working) config. - if effective: - path = ['--show-active-only'] + path - else: - path = ['--show-working-only'] + path - - if isinstance(path, list): - path = " ".join(path) - try: - out = self._run(self._make_command('showConfig', path)) - self.__session_env = save_env - return out - except VyOSError: - self.__session_env = save_env - return(default) + return config_dict - def get_config_dict(self, path=[], effective=False, key_mangling=None): + def get_config_dict(self, path=[], effective=False, key_mangling=None, get_first_key=False): """ - Args: path (str list): Configuration tree path, can be empty - Returns: a dict representation of the config + Args: + path (str list): Configuration tree path, can be empty + effective=False: effective or session config + key_mangling=None: mangle dict keys according to regex and replacement + get_first_key=False: if k = path[:-1], return sub-dict d[k] instead of {k: d[k]} + + Returns: a dict representation of the config under path """ - res = self.show_config(self._make_path(path), effective=effective) - if res: - config_tree = vyos.configtree.ConfigTree(res) - config_dict = json.loads(config_tree.to_json()) - else: - config_dict = {} + config_dict = self.get_cached_dict(effective) + + config_dict = vyos.util.get_sub_dict(config_dict, self._make_path(path), get_first_key) if key_mangling: if not (isinstance(key_mangling, tuple) and \ @@ -308,6 +233,8 @@ class Config(object): raise ValueError("key_mangling must be a tuple of two strings") else: config_dict = vyos.util.mangle_dict_keys(config_dict, key_mangling[0], key_mangling[1]) + else: + config_dict = deepcopy(config_dict) return config_dict @@ -322,12 +249,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isMulti', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_multi(path) def is_tag(self, path): """ @@ -340,12 +263,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isTag', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_tag(path) def is_leaf(self, path): """ @@ -358,12 +277,8 @@ class Config(object): Note: It also returns False if node doesn't exist. """ - try: - path = " ".join(self._level) + " " + path - self._run(self._make_command('isLeaf', path)) - return True - except VyOSError: - return False + self._config_source.set_level(self.get_level) + return self._config_source.is_leaf(path) def return_value(self, path, default=None): """ @@ -524,9 +439,6 @@ class Config(object): Returns: str list: child node names - - Raises: - VyOSError: if the node is not a tag node """ if self._running_config: try: diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index ce086872e..0dc7578d8 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -22,7 +22,6 @@ from enum import Enum from copy import deepcopy from vyos import ConfigError -from vyos.ifconfig import Interface from vyos.validate import is_member from vyos.util import ifname_from_config @@ -97,6 +96,8 @@ def dict_merge(source, destination): for key, value in source.items(): if key not in tmp.keys(): tmp[key] = value + elif isinstance(source[key], dict): + tmp[key] = dict_merge(source[key], tmp[key]) return tmp @@ -214,6 +215,8 @@ def disable_state(conf, check=[3,5,7]): def intf_to_dict(conf, default): + from vyos.ifconfig import Interface + """ Common used function which will extract VLAN related information from config and represent the result as Python dictionary. diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py new file mode 100644 index 000000000..b79893507 --- /dev/null +++ b/python/vyos/configdiff.py @@ -0,0 +1,249 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 enum import IntFlag, auto + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.util import get_sub_dict, mangle_dict_keys +from vyos.xml import defaults + +class ConfigDiffError(Exception): + """ + Raised on config dict access errors, for example, calling get_value on + a non-leaf node. + """ + pass + +def enum_to_key(e): + return e.name.lower() + +class Diff(IntFlag): + MERGE = auto() + DELETE = auto() + ADD = auto() + STABLE = auto() + +requires_effective = [enum_to_key(Diff.DELETE)] +target_defaults = [enum_to_key(Diff.MERGE)] + +def _key_sets_from_dicts(session_dict, effective_dict): + session_keys = list(session_dict) + effective_keys = list(effective_dict) + + ret = {} + stable_keys = [k for k in session_keys if k in effective_keys] + + ret[enum_to_key(Diff.MERGE)] = session_keys + ret[enum_to_key(Diff.DELETE)] = [k for k in effective_keys if k not in stable_keys] + ret[enum_to_key(Diff.ADD)] = [k for k in session_keys if k not in stable_keys] + ret[enum_to_key(Diff.STABLE)] = stable_keys + + return ret + +def _dict_from_key_set(key_set, d): + # This will always be applied to a key_set obtained from a get_sub_dict, + # hence there is no possibility of KeyError, as get_sub_dict guarantees + # a return type of dict + ret = {k: d[k] for k in key_set} + + return ret + +def get_config_diff(config, key_mangling=None): + """ + Check type and return ConfigDiff instance. + """ + if not config or not isinstance(config, Config): + raise TypeError("argument must me a Config instance") + if key_mangling and not (isinstance(key_mangling, tuple) and \ + (len(key_mangling) == 2) and \ + isinstance(key_mangling[0], str) and \ + isinstance(key_mangling[1], str)): + raise ValueError("key_mangling must be a tuple of two strings") + + return ConfigDiff(config, key_mangling) + +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): + self._level = config.get_level() + self._session_config_dict = config.get_cached_dict() + self._effective_config_dict = config.get_cached_dict(effective=True) + self._key_mangling = key_mangling + + # mirrored from Config; allow path arguments relative to level + def _make_path(self, path): + if isinstance(path, str): + path = path.split() + elif isinstance(path, list): + pass + else: + raise TypeError("Path must be a whitespace-separated string or a list") + + ret = self._level + path + return ret + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config dict path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``get_value("name-server")`` is equivalent to calling + ``get_value("system name-server")`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + if isinstance(path, str): + if path: + self._level = path.split() + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def get_level(self): + """ + Gets the current edit level. + + Returns: + str: current edit level + """ + ret = self._level.copy() + return ret + + def _mangle_dict_keys(self, config_dict): + config_dict = mangle_dict_keys(config_dict, self._key_mangling[0], + self._key_mangling[1]) + return config_dict + + def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + """ + Args: + path (str|list): config path + expand_nodes=Diff(0): bit mask of enum indicating for which nodes + to provide full dict; for example, Diff.MERGE + will expand dict['merge'] into dict under + value + no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default + values to ret['merge'] + + Returns: dict of lists, representing differences between session + and effective config, under path + dict['merge'] = session config values + dict['delete'] = effective config values, not in session + dict['add'] = session config values, not in effective + dict['stable'] = config values in both session and effective + """ + session_dict = get_sub_dict(self._session_config_dict, + self._make_path(path), get_first_key=True) + effective_dict = get_sub_dict(self._effective_config_dict, + self._make_path(path), get_first_key=True) + + ret = _key_sets_from_dicts(session_dict, effective_dict) + + if not expand_nodes: + return ret + + for e in Diff: + if expand_nodes & e: + k = enum_to_key(e) + if k in requires_effective: + ret[k] = _dict_from_key_set(ret[k], effective_dict) + else: + ret[k] = _dict_from_key_set(ret[k], session_dict) + + 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 + + def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + """ + Args: + path (str|list): config path + expand_nodes=Diff(0): bit mask of enum indicating for which nodes + to provide full dict; for example, Diff.MERGE + will expand dict['merge'] into dict under + value + no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default + values to ret['merge'] + + Returns: dict of lists, representing differences between session + and effective config, at path + dict['merge'] = session config values + dict['delete'] = effective config values, not in session + dict['add'] = session config values, not in effective + dict['stable'] = config values in both session and effective + """ + session_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) + + ret = _key_sets_from_dicts(session_dict, effective_dict) + + if not expand_nodes: + return ret + + for e in Diff: + if expand_nodes & e: + k = enum_to_key(e) + if k in requires_effective: + ret[k] = _dict_from_key_set(ret[k], effective_dict) + else: + ret[k] = _dict_from_key_set(ret[k], session_dict) + + 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 + + def get_value_diff(self, path=[]): + """ + Args: + path (str|list): config path + + Returns: (new, old) tuple of values in session config/effective config + """ + # one should properly use is_leaf as check; for the moment we will + # deduce from type, which will not catch call on non-leaf node if None + new_value_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + old_value_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) + + new_value = None + old_value = None + if new_value_dict: + new_value = next(iter(new_value_dict.values())) + if old_value_dict: + old_value = next(iter(old_value_dict.values())) + + if new_value and isinstance(new_value, dict): + raise ConfigDiffError("get_value_changed called on non-leaf node") + if old_value and isinstance(old_value, dict): + raise ConfigDiffError("get_value_changed called on non-leaf node") + + return new_value, old_value diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py new file mode 100644 index 000000000..50222e385 --- /dev/null +++ b/python/vyos/configsource.py @@ -0,0 +1,318 @@ + +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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/>. + +import os +import re +import subprocess + +from vyos.configtree import ConfigTree + +class VyOSError(Exception): + """ + Raised on config access errors. + """ + pass + +class ConfigSourceError(Exception): + ''' + Raised on error in ConfigSource subclass init. + ''' + pass + +class ConfigSource: + def __init__(self): + self._running_config: ConfigTree = None + self._session_config: ConfigTree = None + + def get_configtree_tuple(self): + return self._running_config, self._session_config + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + +class ConfigSourceSession(ConfigSource): + def __init__(self, session_env=None): + super().__init__() + self._cli_shell_api = "/bin/cli-shell-api" + self._level = [] + if session_env: + self.__session_env = session_env + else: + self.__session_env = None + + # Running config can be obtained either from op or conf mode, it always succeeds + # once the config system is initialized during boot; + # before initialization, set to empty string + if os.path.isfile('/tmp/vyos-config-status'): + try: + running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) + except VyOSError: + running_config_text = '' + else: + running_config_text = '' + + # Session config ("active") only exists in conf mode. + # In op mode, we'll just use the same running config for both active and session configs. + if self.in_session(): + try: + session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) + except VyOSError: + session_config_text = '' + else: + session_config_text = running_config_text + + if running_config_text: + self._running_config = ConfigTree(running_config_text) + else: + self._running_config = None + + if session_config_text: + self._session_config = ConfigTree(session_config_text) + else: + self._session_config = None + + def _make_command(self, op, path): + args = path.split() + cmd = [self._cli_shell_api, op] + args + return cmd + + def _run(self, cmd): + if self.__session_env: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env) + else: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + out = p.stdout.read() + p.wait() + p.communicate() + if p.returncode != 0: + raise VyOSError() + else: + return out.decode('ascii') + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config tree path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``exists("name-server")`` is equivalent to calling + ``exists("system name-server"`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + # Make sure there's always a space between default path (level) + # and path supplied as method argument + # XXX: for small strings in-place concatenation is not a problem + if isinstance(path, str): + if path: + self._level = re.split(r'\s+', path) + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + try: + self._run(self._make_command('sessionChanged', '')) + return True + except VyOSError: + return False + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + try: + self._run(self._make_command('inSession', '')) + return True + except VyOSError: + return False + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + + # show_config should be independent of CLI edit level. + # Set the CLI edit environment to the top level, and + # restore original on exit. + save_env = self.__session_env + + env_str = self._run(self._make_command('getEditResetEnv', '')) + env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) + root_env = os.environ + for k, v in env_list: + root_env[k] = v + + self.__session_env = root_env + + # FIXUP: by default, showConfig will give you a diff + # if there are uncommitted changes. + # The config parser obviously cannot work with diffs, + # so we need to supress diff production using appropriate + # options for getting either running (active) + # or proposed (working) config. + if effective: + path = ['--show-active-only'] + path + else: + path = ['--show-working-only'] + path + + if isinstance(path, list): + path = " ".join(path) + try: + out = self._run(self._make_command('showConfig', path)) + self.__session_env = save_env + return out + except VyOSError: + self.__session_env = save_env + return(default) + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isMulti', path)) + return True + except VyOSError: + return False + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isTag', path)) + return True + except VyOSError: + return False + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isLeaf', path)) + return True + except VyOSError: + return False + +class ConfigSourceString(ConfigSource): + def __init__(self, running_config_text=None, session_config_text=None): + super().__init__() + + try: + self._running_config = ConfigTree(running_config_text) if running_config_text else None + self._session_config = ConfigTree(session_config_text) if session_config_text else None + except ValueError: + raise ConfigSourceError(f"Init error in {type(self)}") diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py new file mode 100644 index 000000000..32129a048 --- /dev/null +++ b/python/vyos/configverify.py @@ -0,0 +1,78 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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/>. + +# The sole purpose of this module is to hold common functions used in +# all kinds of implementations to verify the CLI configuration. +# It is started by migrating the interfaces to the new get_config_dict() +# approach which will lead to a lot of code that can be reused. + +# NOTE: imports should be as local as possible to the function which +# makes use of it! + +from vyos import ConfigError + +def verify_vrf(config): + """ + Common helper function used by interface implementations to perform + recurring validation of VRF configuration. + """ + from netifaces import interfaces + if 'vrf' in config.keys(): + if config['vrf'] not in interfaces(): + raise ConfigError('VRF "{vrf}" does not exist'.format(**config)) + + if 'is_bridge_member' in config.keys(): + raise ConfigError( + 'Interface "{ifname}" cannot be both a member of VRF "{vrf}" ' + 'and bridge "{is_bridge_member}"!'.format(**config)) + + +def verify_address(config): + """ + Common helper function used by interface implementations to + perform recurring validation of IP address assignmenr + when interface also is part of a bridge. + """ + if {'is_bridge_member', 'address'} <= set(config): + raise ConfigError( + f'Cannot assign address to interface "{ifname}" as it is a ' + f'member of bridge "{is_bridge_member}"!'.format(**config)) + + +def verify_bridge_delete(config): + """ + Common helper function used by interface implementations to + perform recurring validation of IP address assignmenr + when interface also is part of a bridge. + """ + if 'is_bridge_member' in config.keys(): + raise ConfigError( + 'Interface "{ifname}" cannot be deleted as it is a ' + 'member of bridge "{is_bridge_member}"!'.format(**config)) + + +def verify_source_interface(config): + """ + Common helper function used by interface implementations to + perform recurring validation of the existence of a source-interface + required by e.g. peth/MACvlan, MACsec ... + """ + from netifaces import interfaces + if not 'source_interface' in config.keys(): + raise ConfigError('Physical source-interface required for ' + 'interface "{ifname}"'.format(**config)) + if not config['source_interface'] in interfaces(): + raise ConfigError(f'Source interface {source_interface} does not ' + f'exist'.format(**config)) diff --git a/python/vyos/frr.py b/python/vyos/frr.py new file mode 100644 index 000000000..e39b6a914 --- /dev/null +++ b/python/vyos/frr.py @@ -0,0 +1,288 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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/>. + +r""" +A Library for interracting with the FRR daemon suite. +It supports simple configuration manipulation and loading using the official tools +supplied with FRR (vtysh and frr-reload) + +All configuration management and manipulation is done using strings and regex. + + +Example Usage +##### + +# Reading configuration from frr: +``` +>>> original_config = get_configuration() +>>> repr(original_config) +'!\nfrr version 7.3.1\nfrr defaults traditional\nhostname debian\n...... +``` + + +# Modify a configuration section: +``` +>>> new_bgp_section = 'router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n' +>>> modified_config = replace_section(original_config, new_bgp_section, replace_re=r'router bgp \d+') +>>> repr(modified_config) +'............router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n...........' +``` + +Remove a configuration section: +``` +>>> modified_config = remove_section(original_config, r'router ospf') +``` + +Test the new configuration: +``` +>>> try: +>>> mark_configuration(modified configuration) +>>> except ConfigurationNotValid as e: +>>> print('resulting configuration is not valid') +>>> sys.exit(1) +``` + +Apply the new configuration: +``` +>>> try: +>>> replace_configuration(modified_config) +>>> except CommitError as e: +>>> print('Exception while commiting the supplied configuration') +>>> print(e) +>>> exit(1) +``` +""" + +import tempfile +import re +from vyos import util + +_frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', + 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd'] + +path_vtysh = '/usr/bin/vtysh' +path_frr_reload = '/usr/lib/frr/frr-reload.py' + + +class FrrError(Exception): + pass + + +class ConfigurationNotValid(FrrError): + """ + The configuratioin supplied to vtysh is not valid + """ + pass + + +class CommitError(FrrError): + """ + Commiting the supplied configuration failed to commit by a unknown reason + see commit error and/or run mark_configuration on the specified configuration + to se error generated + + used by: reload_configuration() + """ + pass + + +class ConfigSectionNotFound(FrrError): + """ + Removal of configuration failed because it is not existing in the supplied configuration + """ + pass + + +def get_configuration(daemon=None, marked=False): + """ Get current running FRR configuration + daemon: Collect only configuration for the specified FRR daemon, + supplying daemon=None retrieves the complete configuration + marked: Mark the configuration with "end" tags + + return: string containing the running configuration from frr + + """ + if daemon and daemon not in _frr_daemons: + raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') + + cmd = f"{path_vtysh} -c 'show run'" + if daemon: + cmd += f' -d {daemon}' + + output, code = util.popen(cmd, stderr=util.STDOUT) + if code: + raise OSError(code, output) + + config = output.replace('\r', '') + # Remove first header lines from FRR config + config = config.split("\n", 3)[-1] + # Mark the configuration with end tags + if marked: + config = mark_configuration(config) + + return config + + +def mark_configuration(config): + """ Add end marks and Test the configuration for syntax faults + If the configuration is valid a marked version of the configuration is returned, + or else it failes with a ConfigurationNotValid Exception + + config: The configuration string to mark/test + return: The marked configuration from FRR + """ + output, code = util.popen(f"{path_vtysh} -m -f -", stderr=util.STDOUT, input=config) + + if code == 2: + raise ConfigurationNotValid(str(output)) + elif code: + raise OSError(code, output) + + config = output.replace('\r', '') + return config + + +def reload_configuration(config, daemon=None): + """ Execute frr-reload with the new configuration + This will try to reapply the supplied configuration inside FRR. + The configuration needs to be a complete configuration from the integrated config or + from a daemon. + + config: The configuration to apply + daemon: Apply the conigutaion to the specified FRR daemon, + supplying daemon=None applies to the integrated configuration + return: None + """ + if daemon and daemon not in _frr_daemons: + raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') + + f = tempfile.NamedTemporaryFile('w') + f.write(config) + f.flush() + + cmd = f'{path_frr_reload} --reload' + if daemon: + cmd += f' --daemon {daemon}' + cmd += f' {f.name}' + + output, code = util.popen(cmd, stderr=util.STDOUT) + f.close() + if code == 1: + raise CommitError(f'Configuration FRR failed while commiting code: {repr(output)}') + elif code: + raise OSError(code, output) + + return output + + +def execute(command): + """ Run commands inside vtysh + command: str containing commands to execute inside a vtysh session + """ + if not isinstance(command, str): + raise ValueError(f'command needs to be a string: {repr(command)}') + + cmd = f"{path_vtysh} -c '{command}'" + + output, code = util.popen(cmd, stderr=util.STDOUT) + if code: + raise OSError(code, output) + + config = output.replace('\r', '') + return config + + +def configure(lines, daemon=False): + """ run commands inside config mode vtysh + lines: list or str conaining commands to execute inside a configure session + only one command executed on each configure() + Executing commands inside a subcontext uses the list to describe the context + ex: ['router bgp 6500', 'neighbor 192.0.2.1 remote-as 65000'] + return: None + """ + if isinstance(lines, str): + lines = [lines] + elif not isinstance(lines, list): + raise ValueError('lines needs to be string or list of commands') + + if daemon and daemon not in _frr_daemons: + raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') + + cmd = f'{path_vtysh}' + if daemon: + cmd += f' -d {daemon}' + + cmd += " -c 'configure terminal'" + for x in lines: + cmd += f" -c '{x}'" + + output, code = util.popen(cmd, stderr=util.STDOUT) + if code == 1: + raise ConfigurationNotValid(f'Configuration FRR failed: {repr(output)}') + elif code: + raise OSError(code, output) + + config = output.replace('\r', '') + return config + + +def _replace_section(config, replacement, replace_re, before_re): + r"""Replace a section of FRR config + config: full original configuration + replacement: replacement configuration section + replace_re: The regex to replace + example: ^router bgp \d+$.?*^!$ + this will replace everything between ^router bgp X$ and ^!$ + before_re: When replace_re is not existant, the config will be added before this tag + example: ^line vty$ + + return: modified configuration as a text file + """ + # Check if block is configured, remove the existing instance else add a new one + if re.findall(replace_re, config, flags=re.MULTILINE | re.DOTALL): + # Section is in the configration, replace it + return re.sub(replace_re, replacement, config, count=1, + flags=re.MULTILINE | re.DOTALL) + if before_re: + if not re.findall(before_re, config, flags=re.MULTILINE | re.DOTALL): + raise ConfigSectionNotFound(f"Config section {before_re} not found in config") + + # If no section is in the configuration, add it before the line vty line + return re.sub(before_re, rf'{replacement}\n\g<1>', config, count=1, + flags=re.MULTILINE | re.DOTALL) + + raise ConfigSectionNotFound(f"Config section {replacement} not found in config") + + +def replace_section(config, replacement, from_re, to_re=r'!', before_re=r'line vty'): + r"""Replace a section of FRR config + config: full original configuration + replacement: replacement configuration section + from_re: Regex for the start of section matching + example: 'router bgp \d+' + to_re: Regex for stop of section matching + default: '!' + example: '!' or 'end' + before_re: When from_re/to_re does not return a match, the config will + be added before this tag + default: ^line vty$ + + startline and endline tags will be automatically added to the resulting from_re/to_re and before_re regex'es + """ + return _replace_section(config, replacement, replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=rf'^{before_re}$') + + +def remove_section(config, from_re, to_re='!'): + return _replace_section(config, '', replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=None) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 2c2396440..8d7b247fc 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -27,6 +27,7 @@ from netifaces import AF_INET from netifaces import AF_INET6 from vyos import ConfigError +from vyos.configdict import list_diff from vyos.util import mac2eui64 from vyos.validate import is_ipv4 from vyos.validate import is_ipv6 @@ -321,7 +322,11 @@ class Interface(Control): self.set_admin_state('down') self.set_interface('mac', mac) - + + # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion + if prev_state == 'up': + self.set_admin_state('up') + def set_vrf(self, vrf=''): """ Add/Remove interface from given VRF instance. @@ -662,10 +667,12 @@ class Interface(Control): if addr in self._addr: return False + addr_is_v4 = is_ipv4(addr) + # we can't have both DHCP and static IPv4 addresses assigned for a in self._addr: if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or - ( a == 'dhcp' and addr != 'dhcpv6' and is_ipv4(addr) ) ): + ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ): raise ConfigError(( "Can't configure both static IPv4 and DHCP address " "on the same interface")) @@ -676,7 +683,8 @@ class Interface(Control): elif addr == 'dhcpv6': self.dhcp.v6.set() elif not is_intf_addr_assigned(self.ifname, addr): - self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"') + self._cmd(f'ip addr add "{addr}" ' + f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"') else: return False @@ -757,3 +765,41 @@ class Interface(Control): # TODO: port config (STP) return True + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # Update interface description + self.set_alias(config.get('description', None)) + + # Configure assigned interface IP addresses. No longer + # configured addresses will be removed first + new_addr = config.get('address', []) + + # XXX workaround for T2636, convert IP address string to a list + # with one element + if isinstance(new_addr, str): + new_addr = [new_addr] + + # determine IP addresses which are assigned to the interface and build a + # list of addresses which are no longer in the dict so they can be removed + cur_addr = self.get_addr() + for addr in list_diff(cur_addr, new_addr): + self.del_addr(addr) + + for addr in new_addr: + self.add_addr(addr) + + # There are some items in the configuration which can only be applied + # if this instance is not bound to a bridge. This should be checked + # by the caller but better save then sorry! + if not config.get('is_bridge_member', False): + # Bind interface instance into VRF + self.set_vrf(config.get('vrf', '')) + + # Interface administrative state + state = 'down' if 'disable' in config.keys() else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py index 8e4438662..7ebd13b54 100644 --- a/python/vyos/ifconfig/loopback.py +++ b/python/vyos/ifconfig/loopback.py @@ -23,7 +23,7 @@ class LoopbackIf(Interface): The loopback device is a special, virtual network interface that your router uses to communicate with itself. """ - + _persistent_addresses = ['127.0.0.1/8', '::1/128'] default = { 'type': 'loopback', } @@ -49,10 +49,31 @@ class LoopbackIf(Interface): """ # remove all assigned IP addresses from interface for addr in self.get_addr(): - if addr in ["127.0.0.1/8", "::1/128"]: + if addr in self._persistent_addresses: # Do not allow deletion of the default loopback addresses as # this will cause weird system behavior like snmp/ssh no longer # operating as expected, see https://phabricator.vyos.net/T2034. continue self.del_addr(addr) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + addr = config.get('address', []) + # XXX workaround for T2636, convert IP address string to a list + # with one element + if isinstance(addr, str): + addr = [addr] + + # We must ensure that the loopback addresses are never deleted from the system + addr += self._persistent_addresses + + # Update IP address entry in our dictionary + config.update({'address' : addr}) + + # now call the regular function from within our base class + super().update(config) diff --git a/python/vyos/snmpv3_hashgen.py b/python/vyos/snmpv3_hashgen.py new file mode 100644 index 000000000..324c3274d --- /dev/null +++ b/python/vyos/snmpv3_hashgen.py @@ -0,0 +1,50 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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/>. + +# Documentation / Inspiration +# - https://tools.ietf.org/html/rfc3414#appendix-A.3 +# - https://github.com/TheMysteriousX/SNMPv3-Hash-Generator + +key_length = 1048576 + +def random(l): + # os.urandom(8) returns 8 bytes of random data + import os + from binascii import hexlify + return hexlify(os.urandom(l)).decode('utf-8') + +def expand(s, l): + """ repead input string (s) as long as we reach the desired length in bytes """ + from itertools import repeat + reps = l // len(s) + 1 # approximation; worst case: overrun = l + len(s) + return ''.join(list(repeat(s, reps)))[:l].encode('utf-8') + +def plaintext_to_md5(passphrase, engine): + """ Convert input plaintext passphrase to MD5 hashed version usable by net-snmp """ + from hashlib import md5 + tmp = expand(passphrase, key_length) + hash = md5(tmp).digest() + engine = bytearray.fromhex(engine) + out = b''.join([hash, engine, hash]) + return md5(out).digest().hex() + +def plaintext_to_sha1(passphrase, engine): + """ Convert input plaintext passphrase to SHA1hashed version usable by net-snmp """ + from hashlib import sha1 + tmp = expand(passphrase, key_length) + hash = sha1(tmp).digest() + engine = bytearray.fromhex(engine) + out = b''.join([hash, engine, hash]) + return sha1(out).digest().hex() diff --git a/python/vyos/template.py b/python/vyos/template.py index e4b253ed3..d9b0c749d 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -17,11 +17,9 @@ import os from jinja2 import Environment from jinja2 import FileSystemLoader - from vyos.defaults import directories from vyos.util import chmod, chown, makedir - # reuse the same Environment to improve performance _templates_env = { False: Environment(loader=FileSystemLoader(directories['templates'])), @@ -32,6 +30,21 @@ _templates_mem = { True: {}, } +def vyos_address_from_cidr(text): + """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address". + Example: + 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8:: + """ + from ipaddress import ip_network + return ip_network(text).network_address + +def vyos_netmask_from_cidr(text): + """ Take an IPv4/IPv6 CIDR prefix and convert the prefix length to a "subnet mask". + Example: + 192.0.2.0/24 -> 255.255.255.0, 2001:db8::/48 -> ffff:ffff:ffff:: + """ + from ipaddress import ip_network + return ip_network(text).netmask def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None): """ @@ -42,8 +55,8 @@ def render(destination, template, content, trim_blocks=False, formater=None, per This classes cache the renderer, so rendering the same file multiple time does not cause as too much overhead. If use everywhere, it could be changed - and load the template from python environement variables from an import - python module generated when the debian package is build + and load the template from python environement variables from an import + python module generated when the debian package is build (recovering the load time and overhead caused by having the file out of the code) """ @@ -54,11 +67,15 @@ def render(destination, template, content, trim_blocks=False, formater=None, per # Setup a renderer for the given template # This is cached and re-used for performance if template not in _templates_mem[trim_blocks]: - _templates_mem[trim_blocks][template] = _templates_env[trim_blocks].get_template(template) + _env = _templates_env[trim_blocks] + _env.filters['address_from_cidr'] = vyos_address_from_cidr + _env.filters['netmask_from_cidr'] = vyos_netmask_from_cidr + _templates_mem[trim_blocks][template] = _env.get_template(template) + template = _templates_mem[trim_blocks][template] # As we are opening the file with 'w', we are performing the rendering - # before calling open() to not accidentally erase the file if the + # before calling open() to not accidentally erase the file if the # templating fails content = template.render(content) diff --git a/python/vyos/util.py b/python/vyos/util.py index 0ddc14963..7234be6cb 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -364,6 +364,46 @@ def mangle_dict_keys(data, regex, replacement): return new_dict +def _get_sub_dict(d, lpath): + k = lpath[0] + if k not in d.keys(): + return {} + c = {k: d[k]} + lpath = lpath[1:] + if not lpath: + return c + elif not isinstance(c[k], dict): + return {} + return _get_sub_dict(c[k], lpath) + +def get_sub_dict(source, lpath, get_first_key=False): + """ Returns the sub-dict of a nested dict, defined by path of keys. + + Args: + source (dict): Source dict to extract from + lpath (list[str]): sequence of keys + + Returns: source, if lpath is empty, else + {key : source[..]..[key]} for key the last element of lpath, if exists + {} otherwise + """ + if not isinstance(source, dict): + raise TypeError("source must be of type dict") + if not isinstance(lpath, list): + raise TypeError("path must be of type list") + if not lpath: + return source + + ret = _get_sub_dict(source, lpath) + + if get_first_key and lpath and ret: + tmp = next(iter(ret.values())) + if not isinstance(tmp, dict): + raise TypeError("Data under node is not of type dict") + ret = tmp + + return ret + def process_running(pid_file): """ Checks if a process with PID in pid_file is running """ from psutil import pid_exists @@ -612,3 +652,12 @@ def get_bridge_member_config(conf, br, intf): conf.set_level(old_level) return memberconf + +def check_kmod(k_mod): + """ Common utility function to load required kernel modules on demand """ + if isinstance(k_mod, str): + k_mod = k_mod.split() + for module in k_mod: + if not os.path.exists(f'/sys/module/{module}'): + if call(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 9072c5817..a0620e4dd 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -19,6 +19,7 @@ import netifaces import ipaddress from vyos.util import cmd +from vyos import xml # Important note when you are adding new validation functions: # @@ -293,12 +294,12 @@ def is_member(conf, interface, intftype=None): for it in intftype: base = 'interfaces ' + it for intf in conf.list_nodes(base): - memberintf = f'{base} {intf} member interface' - if conf.is_tag(memberintf): + memberintf = [base, intf, 'member', 'interface'] + if xml.is_tag(memberintf): if interface in conf.list_nodes(memberintf): ret_val = intf break - elif conf.is_leaf(memberintf): + elif xml.is_leaf(memberintf): if ( conf.exists(memberintf) and interface in conf.return_values(memberintf) ): ret_val = intf diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py index 52f5bfb38..0f914fed2 100644 --- a/python/vyos/xml/__init__.py +++ b/python/vyos/xml/__init__.py @@ -9,7 +9,7 @@ # 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from vyos.xml import definition @@ -35,5 +35,25 @@ def load_configuration(cache=[]): return xml -def defaults(lpath): - return load_configuration().defaults(lpath) +# def is_multi(lpath): +# return load_configuration().is_multi(lpath) + + +def is_tag(lpath): + return load_configuration().is_tag(lpath) + + +def is_leaf(lpath, flat=True): + return load_configuration().is_leaf(lpath, flat) + + +def defaults(lpath, flat=False): + return load_configuration().defaults(lpath, flat) + + +if __name__ == '__main__': + print(defaults(['service'], flat=True)) + print(defaults(['service'], flat=False)) + + print(is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + print(is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop'])) diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py index c5f6b0fc7..098e64f7e 100644 --- a/python/vyos/xml/definition.py +++ b/python/vyos/xml/definition.py @@ -11,7 +11,6 @@ # You should have received a copy of the GNU Lesser General Public License along with this library; # if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - from vyos.xml import kw # As we index by key, the name is first and then the data: @@ -127,10 +126,12 @@ class XML(dict): elif word: if data_node != kw.plainNode or len(passed) == 1: self.options = [_ for _ in self.tree if _.startswith(word)] + self.options.sort() else: self.options = [] else: self.options = named_options + self.options.sort() self.plain = not is_dataNode @@ -144,6 +145,7 @@ class XML(dict): self.word = '' if self.tree.get(kw.node,'') not in (kw.tagNode, kw.leafNode): self.options = [_ for _ in self.tree if not kw.found(_)] + self.options.sort() def checks(self, cmd): # as we move thought the named node twice @@ -228,8 +230,9 @@ class XML(dict): inner = self.tree[option] prefix = '+> ' if inner.get(kw.node, '') != kw.leafNode else ' ' if kw.help in inner: - h = inner[kw.help] - yield (prefix + option, h.get(kw.summary), '') + yield (prefix + option, inner[kw.help].get(kw.summary), '') + else: + yield (prefix + option, '(no help available)', '') def debug(self): print('------') @@ -245,36 +248,48 @@ class XML(dict): # @lru_cache(maxsize=100) # XXX: need to use cachetool instead - for later - def defaults(self, lpath): + def defaults(self, lpath, flat): d = self[kw.default] for k in lpath: - d = d[k] - r = {} + d = d.get(k, {}) + + if not flat: + r = {} + for k in d: + under = k.replace('-','_') + if isinstance(d[k],dict): + r[under] = self.defaults(lpath + [k], flat) + continue + r[under] = d[k] + return r - def _flatten(inside, index, d, r): + def _flatten(inside, index, d): + r = {} local = inside[index:] prefix = '_'.join(_.replace('-','_') for _ in local) + '_' if local else '' for k in d: under = prefix + k.replace('-','_') level = inside + [k] if isinstance(d[k],dict): - _flatten(level, index, d[k], r) + r.update(_flatten(level, index, d[k])) continue - if self.is_multi(level): + if self.is_multi(level, with_tag=False): r[under] = [_.strip() for _ in d[k].split(',')] continue r[under] = d[k] + return r - _flatten(lpath, len(lpath), d, r) - return r + return _flatten(lpath, len(lpath), d) # from functools import lru_cache # @lru_cache(maxsize=100) # XXX: need to use cachetool instead - for later - def _tree(self, lpath): + def _tree(self, lpath, with_tag=True): """ returns the part of the tree searched or None if it does not exists + if with_tag is set, this is a configuration path (with tagNode names) + and tag name will be removed from the path when traversing the tree """ tree = self[kw.tree] spath = lpath.copy() @@ -283,19 +298,33 @@ class XML(dict): if p not in tree: return None tree = tree[p] + if with_tag and spath and tree[kw.node] == kw.tagNode: + spath.pop(0) return tree - def _get(self, lpath, tag): - return self._tree(lpath + [tag]) - - def is_multi(self, lpath): - return self._get(lpath, kw.multi) is True - - def is_tag(self, lpath): - return self._get(lpath, kw.node) == kw.tagNode - - def is_leaf(self, lpath): - return self._get(lpath, kw.node) == kw.leafNode - - def exists(self, lpath): - return self._get(lpath, kw.node) is not None + def _get(self, lpath, tag, with_tag=True): + tree = self._tree(lpath, with_tag) + if tree is None: + return None + return tree.get(tag, None) + + def is_multi(self, lpath, with_tag=True): + tree = self._get(lpath, kw.multi, with_tag) + if tree is None: + return None + return tree is True + + def is_tag(self, lpath, with_tag=True): + tree = self._get(lpath, kw.node, with_tag) + if tree is None: + return None + return tree == kw.tagNode + + def is_leaf(self, lpath, with_tag=True): + tree = self._get(lpath, kw.node, with_tag) + if tree is None: + return None + return tree == kw.leafNode + + def exists(self, lpath, with_tag=True): + return self._get(lpath, kw.node, with_tag) is not None diff --git a/python/vyos/xml/kw.py b/python/vyos/xml/kw.py index c85d9e0fd..64521c51a 100644 --- a/python/vyos/xml/kw.py +++ b/python/vyos/xml/kw.py @@ -27,12 +27,12 @@ def found(word): # root -version = '(version)' -tree = '(tree)' -priorities = '(priorities)' -owners = '(owners)' -tags = '(tags)' -default = '(default)' +version = '[version]' +tree = '[tree]' +priorities = '[priorities]' +owners = '[owners]' +tags = '[tags]' +default = '[default]' # nodes diff --git a/python/vyos/xml/test_xml.py b/python/vyos/xml/test_xml.py index ac0620d99..ff55151d2 100644 --- a/python/vyos/xml/test_xml.py +++ b/python/vyos/xml/test_xml.py @@ -33,7 +33,7 @@ class TestSearch(TestCase): last = self.xml.traverse("") self.assertEqual(last, '') self.assertEqual(self.xml.inside, []) - self.assertEqual(self.xml.options, ['protocols', 'service', 'system', 'firewall', 'interfaces', 'vpn', 'nat', 'vrf', 'high-availability']) + self.assertEqual(self.xml.options, ['firewall', 'high-availability', 'interfaces', 'nat', 'protocols', 'service', 'system', 'vpn', 'vrf']) self.assertEqual(self.xml.filling, False) self.assertEqual(self.xml.word, last) self.assertEqual(self.xml.check, False) |