summaryrefslogtreecommitdiff
path: root/python/vyos
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos')
-rw-r--r--python/vyos/config.py184
-rw-r--r--python/vyos/configdict.py5
-rw-r--r--python/vyos/configdiff.py249
-rw-r--r--python/vyos/configsource.py318
-rw-r--r--python/vyos/configverify.py78
-rw-r--r--python/vyos/frr.py288
-rw-r--r--python/vyos/ifconfig/interface.py52
-rw-r--r--python/vyos/ifconfig/loopback.py25
-rw-r--r--python/vyos/snmpv3_hashgen.py50
-rw-r--r--python/vyos/template.py29
-rw-r--r--python/vyos/util.py49
-rw-r--r--python/vyos/validate.py7
-rw-r--r--python/vyos/xml/__init__.py26
-rw-r--r--python/vyos/xml/definition.py81
-rw-r--r--python/vyos/xml/kw.py12
-rw-r--r--python/vyos/xml/test_xml.py2
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)