summaryrefslogtreecommitdiff
path: root/python/vyos
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos')
-rw-r--r--python/vyos/__init__.py1
-rw-r--r--python/vyos/airbag.py181
-rw-r--r--python/vyos/authutils.py41
-rw-r--r--python/vyos/base.py18
-rw-r--r--python/vyos/certbot_util.py58
-rw-r--r--python/vyos/component_versions.py57
-rw-r--r--python/vyos/config.py454
-rw-r--r--python/vyos/configdict.py314
-rw-r--r--python/vyos/configdiff.py249
-rw-r--r--python/vyos/configsession.py191
-rw-r--r--python/vyos/configsource.py318
-rw-r--r--python/vyos/configtree.py283
-rw-r--r--python/vyos/configverify.py139
-rw-r--r--python/vyos/debug.py205
-rw-r--r--python/vyos/defaults.py53
-rw-r--r--python/vyos/dicts.py53
-rw-r--r--python/vyos/formatversions.py109
-rw-r--r--python/vyos/frr.py288
-rw-r--r--python/vyos/hostsd_client.py119
-rw-r--r--python/vyos/ifconfig/__init__.py44
-rw-r--r--python/vyos/ifconfig/afi.py19
-rw-r--r--python/vyos/ifconfig/bond.py383
-rw-r--r--python/vyos/ifconfig/bridge.py263
-rw-r--r--python/vyos/ifconfig/control.py185
-rw-r--r--python/vyos/ifconfig/dummy.py56
-rw-r--r--python/vyos/ifconfig/ethernet.py309
-rw-r--r--python/vyos/ifconfig/geneve.py85
-rw-r--r--python/vyos/ifconfig/input.py31
-rw-r--r--python/vyos/ifconfig/interface.py1067
-rw-r--r--python/vyos/ifconfig/l2tpv3.py113
-rw-r--r--python/vyos/ifconfig/loopback.py89
-rw-r--r--python/vyos/ifconfig/macsec.py92
-rw-r--r--python/vyos/ifconfig/macvlan.py89
-rw-r--r--python/vyos/ifconfig/operational.py179
-rw-r--r--python/vyos/ifconfig/pppoe.py41
-rw-r--r--python/vyos/ifconfig/section.py189
-rw-r--r--python/vyos/ifconfig/stp.py70
-rw-r--r--python/vyos/ifconfig/tunnel.py338
-rw-r--r--python/vyos/ifconfig/vlan.py142
-rw-r--r--python/vyos/ifconfig/vrrp.py151
-rw-r--r--python/vyos/ifconfig/vti.py31
-rw-r--r--python/vyos/ifconfig/vtun.py44
-rw-r--r--python/vyos/ifconfig/vxlan.py130
-rw-r--r--python/vyos/ifconfig/wireguard.py247
-rw-r--r--python/vyos/ifconfig/wireless.py102
-rw-r--r--python/vyos/iflag.py38
-rw-r--r--python/vyos/initialsetup.py72
-rw-r--r--python/vyos/ioctl.py36
-rw-r--r--python/vyos/limericks.py72
-rw-r--r--python/vyos/logger.py143
-rw-r--r--python/vyos/migrator.py220
-rw-r--r--python/vyos/remote.py143
-rw-r--r--python/vyos/snmpv3_hashgen.py50
-rw-r--r--python/vyos/systemversions.py63
-rw-r--r--python/vyos/template.py143
-rw-r--r--python/vyos/util.py688
-rw-r--r--python/vyos/validate.py331
-rw-r--r--python/vyos/version.py107
-rw-r--r--python/vyos/xml/.gitignore1
-rw-r--r--python/vyos/xml/__init__.py59
-rw-r--r--python/vyos/xml/cache/__init__.py0
-rw-r--r--python/vyos/xml/definition.py330
-rwxr-xr-xpython/vyos/xml/generate.py70
-rw-r--r--python/vyos/xml/kw.py83
-rw-r--r--python/vyos/xml/load.py290
-rw-r--r--python/vyos/xml/test_xml.py279
66 files changed, 10838 insertions, 0 deletions
diff --git a/python/vyos/__init__.py b/python/vyos/__init__.py
new file mode 100644
index 000000000..e3e14fdd8
--- /dev/null
+++ b/python/vyos/__init__.py
@@ -0,0 +1 @@
+from .base import ConfigError
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
new file mode 100644
index 000000000..510ab7f46
--- /dev/null
+++ b/python/vyos/airbag.py
@@ -0,0 +1,181 @@
+# Copyright 2019-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 sys
+from datetime import datetime
+
+from vyos import debug
+from vyos.logger import syslog
+from vyos.version import get_version
+from vyos.version import get_full_version_data
+
+
+def enable(log=True):
+ if log:
+ _intercepting_logger()
+ _intercepting_exceptions()
+
+
+_noteworthy = []
+
+
+def noteworthy(msg):
+ """
+ noteworthy can be use to take note things which we may not want to
+ report to the user may but be worth including in bug report
+ if something goes wrong later on
+ """
+ _noteworthy.append(msg)
+
+
+# emulate a file object
+class _IO(object):
+ def __init__(self, std, log):
+ self.std = std
+ self.log = log
+
+ def write(self, message):
+ self.std.write(message)
+ for line in message.split('\n'):
+ s = line.rstrip()
+ if s:
+ self.log(s)
+
+ def flush(self):
+ self.std.flush()
+
+ def close(self):
+ pass
+
+
+# The function which will be used to report information
+# to users when an exception is unhandled
+def bug_report(dtype, value, trace):
+ from traceback import format_exception
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ information = get_full_version_data()
+ trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n')
+ note = ''
+ if _noteworthy:
+ note = 'noteworthy:\n'
+ note += '\n'.join(_noteworthy)
+
+ information.update({
+ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ 'trace': trace,
+ 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
+ 'note': note,
+ })
+
+ sys.stdout.write(INTRO.format(**information))
+ sys.stdout.flush()
+
+ sys.stderr.write(FAULT.format(**information))
+ sys.stderr.flush()
+
+
+# define an exception handler to be run when an exception
+# reach the end of __main__ and was not intercepted
+def _intercepter(dtype, value, trace):
+ bug_report(dtype, value, trace)
+ if debug.enabled('developer'):
+ import pdb
+ pdb.pm()
+
+
+def _intercepting_logger(_singleton=[False]):
+ skip = _singleton.pop()
+ _singleton.append(True)
+ if skip:
+ return
+
+ # log to syslog any message sent to stderr
+ sys.stderr = _IO(sys.stderr, syslog.critical)
+
+
+# lists as default arguments in function is normally dangerous
+# as they will keep any modification performed, unless this is
+# what you want to do (in that case to only run the code once)
+def _intercepting_exceptions(_singleton=[False]):
+ skip = _singleton.pop()
+ _singleton.append(True)
+ if skip:
+ return
+
+ # install the handler to replace the default behaviour
+ # which just prints the exception trace on screen
+ sys.excepthook = _intercepter
+
+
+# Messages to print
+# if the key before the value has not time, syslog takes that as the source of the message
+
+FAULT = """\
+Report Time: {date}
+Image Version: VyOS {version}
+Release Train: {release_train}
+
+Built by: {built_by}
+Built on: {built_on}
+Build UUID: {build_uuid}
+Build Commit ID: {build_git}
+
+Architecture: {system_arch}
+Boot via: {boot_via}
+System type: {system_type}
+
+Hardware vendor: {hardware_vendor}
+Hardware model: {hardware_model}
+Hardware S/N: {hardware_serial}
+Hardware UUID: {hardware_uuid}
+
+{trace}
+{note}
+"""
+
+INTRO = """\
+VyOS had an issue completing a command.
+
+We are sorry that you encountered a problem while using VyOS.
+There are a few things you can do to help us (and yourself):
+{instructions}
+
+When reporting problems, please include as much information as possible:
+- do not obfuscate any data (feel free to contact us privately if your
+ business policy requires it)
+- and include all the information presented below
+
+"""
+
+COMMUNITY = """\
+- Make sure you are running the latest version of the code available at
+ https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso
+- Consult the forum to see how to handle this issue
+ https://forum.vyos.io
+- Join our community on slack where our users exchange help and advice
+ https://vyos.slack.com
+""".strip()
+
+SUPPORTED = """\
+- Make sure you are running the latest stable version of VyOS
+ the code is available at https://downloads.vyos.io/?dir=release/current
+- Contact us using the online help desk
+ https://support.vyos.io/
+- Join our community on slack where our users exchange help and advice
+ https://vyos.slack.com
+""".strip()
diff --git a/python/vyos/authutils.py b/python/vyos/authutils.py
new file mode 100644
index 000000000..66b5f4a74
--- /dev/null
+++ b/python/vyos/authutils.py
@@ -0,0 +1,41 @@
+# authutils -- miscelanneous functions for handling passwords and publis keys
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import re
+
+from vyos.util import cmd
+
+
+def make_password_hash(password):
+ """ Makes a password hash for /etc/shadow using mkpasswd """
+
+ mkpassword = 'mkpasswd --method=sha-512 --stdin'
+ return cmd(mkpassword, input=password, timeout=5)
+
+def split_ssh_public_key(key_string, defaultname=""):
+ """ Splits an SSH public key into its components """
+
+ key_string = key_string.strip()
+ parts = re.split(r'\s+', key_string)
+
+ if len(parts) == 3:
+ key_type, key_data, key_name = parts[0], parts[1], parts[2]
+ else:
+ key_type, key_data, key_name = parts[0], parts[1], defaultname
+
+ if key_type not in ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']:
+ raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type))
+
+ return({"type": key_type, "data": key_data, "name": key_name})
diff --git a/python/vyos/base.py b/python/vyos/base.py
new file mode 100644
index 000000000..4e23714e5
--- /dev/null
+++ b/python/vyos/base.py
@@ -0,0 +1,18 @@
+# Copyright 2018 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/>.
+
+
+class ConfigError(Exception):
+ pass
diff --git a/python/vyos/certbot_util.py b/python/vyos/certbot_util.py
new file mode 100644
index 000000000..df42d4780
--- /dev/null
+++ b/python/vyos/certbot_util.py
@@ -0,0 +1,58 @@
+# certbot_util -- adaptation of certbot_nginx name matching functions for VyOS
+# https://github.com/certbot/certbot/blob/master/LICENSE.txt
+
+from certbot_nginx import parser
+
+NAME_RANK = 0
+START_WILDCARD_RANK = 1
+END_WILDCARD_RANK = 2
+REGEX_RANK = 3
+
+def _rank_matches_by_name(server_block_list, target_name):
+ """Returns a ranked list of server_blocks that match target_name.
+ Adapted from the function of the same name in
+ certbot_nginx.NginxConfigurator
+ """
+ matches = []
+ for server_block in server_block_list:
+ name_type, name = parser.get_best_match(target_name,
+ server_block['name'])
+ if name_type == 'exact':
+ matches.append({'vhost': server_block,
+ 'name': name,
+ 'rank': NAME_RANK})
+ elif name_type == 'wildcard_start':
+ matches.append({'vhost': server_block,
+ 'name': name,
+ 'rank': START_WILDCARD_RANK})
+ elif name_type == 'wildcard_end':
+ matches.append({'vhost': server_block,
+ 'name': name,
+ 'rank': END_WILDCARD_RANK})
+ elif name_type == 'regex':
+ matches.append({'vhost': server_block,
+ 'name': name,
+ 'rank': REGEX_RANK})
+
+ return sorted(matches, key=lambda x: x['rank'])
+
+def _select_best_name_match(matches):
+ """Returns the best name match of a ranked list of server_blocks.
+ Adapted from the function of the same name in
+ certbot_nginx.NginxConfigurator
+ """
+ if not matches:
+ return None
+ elif matches[0]['rank'] in [START_WILDCARD_RANK, END_WILDCARD_RANK]:
+ rank = matches[0]['rank']
+ wildcards = [x for x in matches if x['rank'] == rank]
+ return max(wildcards, key=lambda x: len(x['name']))['vhost']
+ else:
+ return matches[0]['vhost']
+
+def choose_server_block(server_block_list, target_name):
+ matches = _rank_matches_by_name(server_block_list, target_name)
+ server_blocks = [x for x in [_select_best_name_match(matches)]
+ if x is not None]
+ return server_blocks
+
diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py
new file mode 100644
index 000000000..90b458aae
--- /dev/null
+++ b/python/vyos/component_versions.py
@@ -0,0 +1,57 @@
+# Copyright 2017 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 version data looks like:
+
+/* Warning: Do not remove the following line. */
+/* === vyatta-config-version:
+"cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@1:dhcp-server@4:firewall@5:ipsec@4:nat@4:qos@1:quagga@2:system@8:vrrp@1:wanloadbalance@3:webgui@1:webproxy@1:zone-policy@1"
+=== */
+/* Release version: 1.2.0-rolling+201806131737 */
+"""
+
+import re
+
+def get_component_version(string_line):
+ """
+ Get component version dictionary from string
+ return empty dictionary if string contains no config information
+ or raise error if component version string malformed
+ """
+ return_value = {}
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line):
+
+ if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line):
+ raise ValueError("malformed configuration string: " + str(string_line))
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
+ if pair[0] in return_value.keys():
+ raise ValueError("duplicate unit name: \"" + str(pair[0]) + "\" in string: \"" + string_line + "\"")
+ return_value[pair[0]] = int(pair[1])
+
+ return return_value
+
+
+def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/config.boot'):
+ """
+ Get component version dictionary parsing config file line by line
+ """
+ f = open(config_file_name, 'r')
+ for line_in_config in f:
+ component_version = get_component_version(line_in_config)
+ if component_version:
+ return component_version
+ raise ValueError("no config string in file:", config_file_name)
diff --git a/python/vyos/config.py b/python/vyos/config.py
new file mode 100644
index 000000000..884d6d947
--- /dev/null
+++ b/python/vyos/config.py
@@ -0,0 +1,454 @@
+# Copyright 2017, 2019 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/>.
+
+"""
+A library for reading VyOS running config data.
+
+This library is used internally by all config scripts of VyOS,
+but its API should be considered stable and safe to use
+in user scripts.
+
+Note that this module will not work outside VyOS.
+
+Node taxonomy
+#############
+
+There are multiple types of config tree nodes in VyOS, each requires
+its own set of operations.
+
+*Leaf nodes* (such as "address" in interfaces) can have values, but cannot
+have children.
+Leaf nodes can have one value, multiple values, or no values at all.
+
+For example, "system host-name" is a single-value leaf node,
+"system name-server" is a multi-value leaf node (commonly abbreviated "multi node"),
+and "system ip disable-forwarding" is a valueless leaf node.
+
+Non-leaf nodes cannot have values, but they can have child nodes. They are divided into
+two classes depending on whether the names of their children are fixed or not.
+For example, under "system", the names of all valid child nodes are predefined
+("login", "name-server" etc.).
+
+To the contrary, children of the "system task-scheduler task" node can have arbitrary names.
+Such nodes are called *tag nodes*. This terminology is confusing but we keep using it for lack
+of a better word. No one remembers if the "tag" in "task Foo" is "task" or "Foo",
+but the distinction is irrelevant in practice.
+
+Configuration modes
+###################
+
+VyOS has two distinct modes: operational mode and configuration mode. When a user logins,
+the CLI is in the operational mode. In this mode, only the running (effective) config is accessible for reading.
+
+When a user enters the "configure" command, a configuration session is setup. Every config session
+has its *proposed* (or *session*) config built on top of the current running config. When changes are commited, if commit succeeds,
+the proposed config is merged into the running config.
+
+In configuration mode, "base" functions like `exists`, `return_value` return values from the session config,
+while functions prefixed "effective" return values from the running config.
+
+In operational mode, all functions return values from the running config.
+
+"""
+
+import re
+import json
+from copy import deepcopy
+
+import vyos.util
+import vyos.configtree
+from vyos.configsource import ConfigSource, ConfigSourceSession
+
+class Config(object):
+ """
+ The class of config access objects.
+
+ Internally, in the current implementation, this object is *almost* stateless,
+ the only state it keeps is relative *config path* for convenient access to config
+ subtrees.
+ """
+ def __init__(self, session_env=None, config_source=None):
+ if config_source is None:
+ self._config_source = ConfigSourceSession(session_env)
+ else:
+ if not isinstance(config_source, ConfigSource):
+ raise TypeError("config_source not of type ConfigSource")
+ self._config_source = config_source
+
+ 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
+ # libvyosconfig paths are lists, but since node names cannot contain whitespace,
+ # splitting at whitespace is reasonably safe.
+ # It may cause problems with exists() when it's used for checking values,
+ # since values may contain whitespace.
+ if isinstance(path, str):
+ path = re.split(r'\s+', path)
+ elif isinstance(path, list):
+ pass
+ else:
+ raise TypeError("Path must be a whitespace-separated string or a list")
+ return (self._level + path)
+
+ 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 get_level(self):
+ """
+ Gets the current edit level.
+
+ Returns:
+ str: current edit level
+ """
+ return(self._level.copy())
+
+ def exists(self, path):
+ """
+ Checks if a node with given path exists in the running or proposed config
+
+ Returns:
+ True if node exists, False otherwise
+
+ Note:
+ This function cannot be used outside a configuration sessions.
+ In operational mode scripts, use ``exists_effective``.
+ """
+ if not self._session_config:
+ return False
+ if self._session_config.exists(self._make_path(path)):
+ return True
+ else:
+ # libvyosconfig exists() works only for _nodes_, not _values_
+ # libvyattacfg one also worked for values, so we emulate that case here
+ if isinstance(path, str):
+ path = re.split(r'\s+', path)
+ path_without_value = path[:-1]
+ path_str = " ".join(path_without_value)
+ try:
+ value = self._session_config.return_value(self._make_path(path_str))
+ return (value == path[-1])
+ except vyos.configtree.ConfigTreeError:
+ # node doesn't exist at all
+ return False
+
+ def session_changed(self):
+ """
+ Returns:
+ True if the config session has uncommited changes, False otherwise.
+ """
+ return self._config_source.session_changed()
+
+ def in_session(self):
+ """
+ Returns:
+ True if called from a configuration session, False otherwise.
+ """
+ return self._config_source.in_session()
+
+ 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
+ """
+ return self._config_source.show_config(path, default, effective)
+
+ def get_cached_dict(self, effective=False):
+ cached = self._dict_cache.get(effective, {})
+ if cached:
+ config_dict = cached
+ else:
+ config_dict = {}
+
+ 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._dict_cache[effective] = config_dict
+
+ return config_dict
+
+ 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
+ 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
+ """
+ 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 \
+ (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")
+ 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
+
+ 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.
+ """
+ self._config_source.set_level(self.get_level)
+ return self._config_source.is_multi(path)
+
+ 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.
+ """
+ self._config_source.set_level(self.get_level)
+ return self._config_source.is_tag(path)
+
+ 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.
+ """
+ self._config_source.set_level(self.get_level)
+ return self._config_source.is_leaf(path)
+
+ def return_value(self, path, default=None):
+ """
+ Retrieve a value of single-value leaf node in the running or proposed config
+
+ Args:
+ path (str): Configuration tree path
+ default (str): Default value to return if node does not exist
+
+ Returns:
+ str: Node value, if it has any
+ None: if node is valueless *or* if it doesn't exist
+
+ Note:
+ Due to the issue with treatment of valueless nodes by this function,
+ valueless nodes should be checked with ``exists`` instead.
+
+ This function cannot be used outside a configuration session.
+ In operational mode scripts, use ``return_effective_value``.
+ """
+ if self._session_config:
+ try:
+ value = self._session_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+ else:
+ value = None
+
+ if not value:
+ return(default)
+ else:
+ return(value)
+
+ def return_values(self, path, default=[]):
+ """
+ Retrieve all values of a multi-value leaf node in the running or proposed config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ str list: Node values, if it has any
+ []: if node does not exist
+
+ Note:
+ This function cannot be used outside a configuration session.
+ In operational mode scripts, use ``return_effective_values``.
+ """
+ if self._session_config:
+ try:
+ values = self._session_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+ else:
+ values = []
+
+ if not values:
+ return(default.copy())
+ else:
+ return(values)
+
+ def list_nodes(self, path, default=[]):
+ """
+ Retrieve names of all children of a tag node in the running or proposed config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ string list: child node names
+
+ """
+ if self._session_config:
+ try:
+ nodes = self._session_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+ else:
+ nodes = []
+
+ if not nodes:
+ return(default.copy())
+ else:
+ return(nodes)
+
+ def exists_effective(self, path):
+ """
+ Check if a node exists in the running (effective) config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ True if node exists in the running config, False otherwise
+
+ Note:
+ This function is safe to use in operational mode. In configuration mode,
+ it ignores uncommited changes.
+ """
+ if self._running_config:
+ return(self._running_config.exists(self._make_path(path)))
+
+ return False
+
+ def return_effective_value(self, path, default=None):
+ """
+ Retrieve a values of a single-value leaf node in a running (effective) config
+
+ Args:
+ path (str): Configuration tree path
+ default (str): Default value to return if node does not exist
+
+ Returns:
+ str: Node value
+ """
+ if self._running_config:
+ try:
+ value = self._running_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+ else:
+ value = None
+
+ if not value:
+ return(default)
+ else:
+ return(value)
+
+ def return_effective_values(self, path, default=[]):
+ """
+ Retrieve all values of a multi-value node in a running (effective) config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ str list: A list of values
+ """
+ if self._running_config:
+ try:
+ values = self._running_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+ else:
+ values = []
+
+ if not values:
+ return(default.copy())
+ else:
+ return(values)
+
+ def list_effective_nodes(self, path, default=[]):
+ """
+ Retrieve names of all children of a tag node in the running config
+
+ Args:
+ path (str): Configuration tree path
+
+ Returns:
+ str list: child node names
+ """
+ if self._running_config:
+ try:
+ nodes = self._running_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+ else:
+ nodes = []
+
+ if not nodes:
+ return(default.copy())
+ else:
+ return(nodes)
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
new file mode 100644
index 000000000..bd8624ced
--- /dev/null
+++ b/python/vyos/configdict.py
@@ -0,0 +1,314 @@
+# Copyright 2019 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/>.
+
+"""
+A library for retrieving value dicts from VyOS configs in a declarative fashion.
+"""
+import os
+
+from enum import Enum
+from copy import deepcopy
+
+from vyos import ConfigError
+
+def retrieve_config(path_hash, base_path, config):
+ """
+ Retrieves a VyOS config as a dict according to a declarative description
+
+ The description dict, passed in the first argument, must follow this format:
+ ``field_name : <path, type, [inner_options_dict]>``.
+
+ Supported types are: ``str`` (for normal nodes),
+ ``list`` (returns a list of strings, for multi nodes),
+ ``bool`` (returns True if valueless node exists),
+ ``dict`` (for tag nodes, returns a dict indexed by node names,
+ according to description in the third item of the tuple).
+
+ Args:
+ path_hash (dict): Declarative description of the config to retrieve
+ base_path (list): A base path to prepend to all option paths
+ config (vyos.config.Config): A VyOS config object
+
+ Returns:
+ dict: config dict
+ """
+ config_hash = {}
+
+ for k in path_hash:
+
+ if type(path_hash[k]) != tuple:
+ raise ValueError("In field {0}: expected a tuple, got a value {1}".format(k, str(path_hash[k])))
+ if len(path_hash[k]) < 2:
+ raise ValueError("In field {0}: field description must be a tuple of at least two items, path (list) and type".format(k))
+
+ path = path_hash[k][0]
+ if type(path) != list:
+ raise ValueError("In field {0}: path must be a list, not a {1}".format(k, type(path)))
+
+ typ = path_hash[k][1]
+ if type(typ) != type:
+ raise ValueError("In field {0}: type must be a type, not a {1}".format(k, type(typ)))
+
+ path = base_path + path
+
+ path_str = " ".join(path)
+
+ if typ == str:
+ config_hash[k] = config.return_value(path_str)
+ elif typ == list:
+ config_hash[k] = config.return_values(path_str)
+ elif typ == bool:
+ config_hash[k] = config.exists(path_str)
+ elif typ == dict:
+ try:
+ inner_hash = path_hash[k][2]
+ except IndexError:
+ raise ValueError("The type of the \'{0}\' field is dict, but inner options hash is missing from the tuple".format(k))
+ config_hash[k] = {}
+ nodes = config.list_nodes(path_str)
+ for node in nodes:
+ config_hash[k][node] = retrieve_config(inner_hash, path + [node], config)
+
+ return config_hash
+
+
+def dict_merge(source, destination):
+ """ Merge two dictionaries. Only keys which are not present in destination
+ will be copied from source, anything else will be kept untouched. Function
+ will return a new dict which has the merged key/value pairs. """
+ from copy import deepcopy
+ tmp = deepcopy(destination)
+
+ for key, value in source.items():
+ if key not in tmp:
+ tmp[key] = value
+ elif isinstance(source[key], dict):
+ tmp[key] = dict_merge(source[key], tmp[key])
+
+ return tmp
+
+def list_diff(first, second):
+ """ Diff two dictionaries and return only unique items """
+ second = set(second)
+ return [item for item in first if item not in second]
+
+def T2665_default_dict_cleanup(dict):
+ """ Cleanup default keys for tag nodes https://phabricator.vyos.net/T2665. """
+ # Cleanup
+ for vif in ['vif', 'vif_s']:
+ if vif in dict:
+ for key in ['ip', 'mtu', 'dhcpv6_options']:
+ if key in dict[vif]:
+ del dict[vif][key]
+
+ # cleanup VIF-S defaults
+ if 'vif_c' in dict[vif]:
+ for key in ['ip', 'mtu', 'dhcpv6_options']:
+ if key in dict[vif]['vif_c']:
+ del dict[vif]['vif_c'][key]
+ # If there is no vif-c defined and we just cleaned the default
+ # keys - we can clean the entire vif-c dict as it's useless
+ if not dict[vif]['vif_c']:
+ del dict[vif]['vif_c']
+
+ # If there is no real vif/vif-s defined and we just cleaned the default
+ # keys - we can clean the entire vif dict as it's useless
+ if not dict[vif]:
+ del dict[vif]
+
+ if 'dhcpv6_options' in dict and 'pd' in dict['dhcpv6_options']:
+ if 'length' in dict['dhcpv6_options']['pd']:
+ del dict['dhcpv6_options']['pd']['length']
+
+ # delete empty dicts
+ if 'dhcpv6_options' in dict:
+ if 'pd' in dict['dhcpv6_options']:
+ # test if 'pd' is an empty node so we can remove it
+ if not dict['dhcpv6_options']['pd']:
+ del dict['dhcpv6_options']['pd']
+
+ # test if 'dhcpv6_options' is an empty node so we can remove it
+ if not dict['dhcpv6_options']:
+ del dict['dhcpv6_options']
+
+ return dict
+
+def leaf_node_changed(conf, path):
+ """
+ Check if a leaf node was altered. If it has been altered - values has been
+ changed, or it was added/removed, we will return the old value. If nothing
+ has been changed, None is returned
+ """
+ from vyos.configdiff import get_config_diff
+ D = get_config_diff(conf, key_mangling=('-', '_'))
+ D.set_level(conf.get_level())
+ (new, old) = D.get_value_diff(path)
+ if new != old:
+ if isinstance(old, str):
+ return old
+ elif isinstance(old, list):
+ if isinstance(new, str):
+ new = [new]
+ elif isinstance(new, type(None)):
+ new = []
+ return list_diff(old, new)
+
+ return None
+
+def node_changed(conf, path):
+ """
+ Check if a leaf node was altered. If it has been altered - values has been
+ changed, or it was added/removed, we will return the old value. If nothing
+ has been changed, None is returned
+ """
+ from vyos.configdiff import get_config_diff, Diff
+ D = get_config_diff(conf, key_mangling=('-', '_'))
+ D.set_level(conf.get_level())
+ # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
+ keys = D.get_child_nodes_diff(path, expand_nodes=Diff.DELETE)['delete'].keys()
+ return list(keys)
+
+def get_removed_vlans(conf, dict):
+ """
+ Common function to parse a dictionary retrieved via get_config_dict() and
+ determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q.
+ """
+ from vyos.configdiff import get_config_diff, Diff
+
+ # Check vif, vif-s/vif-c VLAN interfaces for removal
+ D = get_config_diff(conf, key_mangling=('-', '_'))
+ D.set_level(conf.get_level())
+ # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
+ keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys()
+ if keys:
+ dict.update({'vif_remove': [*keys]})
+
+ # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
+ keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys()
+ if keys:
+ dict.update({'vif_s_remove': [*keys]})
+
+ for vif in dict.get('vif_s', {}).keys():
+ keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys()
+ if keys:
+ dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}})
+
+ return dict
+
+
+def dict_add_dhcpv6pd_defaults(defaults, config_dict):
+ # Implant default dictionary for DHCPv6-PD instances
+ if 'dhcpv6_options' in config_dict and 'pd' in config_dict['dhcpv6_options']:
+ for pd, pd_config in config_dict['dhcpv6_options']['pd'].items():
+ config_dict['dhcpv6_options']['pd'][pd] = dict_merge(
+ defaults, pd_config)
+
+ return config_dict
+
+def get_interface_dict(config, base, ifname=''):
+ """
+ Common utility function to retrieve and mandgle the interfaces available
+ in CLI configuration. All interfaces have a common base ground where the
+ value retrival is identical - so it can and should be reused
+
+ Will return a dictionary with the necessary interface configuration
+ """
+ from vyos.util import vyos_dict_search
+ from vyos.validate import is_member
+ from vyos.xml import defaults
+
+ if not ifname:
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+
+ # retrieve interface default values
+ default_values = defaults(base)
+
+ # setup config level which is extracted in get_removed_vlans()
+ config.set_level(base + [ifname])
+ dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+
+ # Check if interface has been removed
+ if dict == {}:
+ dict.update({'deleted' : ''})
+
+ # Add interface instance name into dictionary
+ dict.update({'ifname': ifname})
+
+ # We have gathered the dict representation of the CLI, but there are
+ # default options which we need to update into the dictionary
+ # retrived.
+ dict = dict_merge(default_values, dict)
+
+ # Check if we are a member of a bridge device
+ bridge = is_member(config, ifname, 'bridge')
+ if bridge:
+ dict.update({'is_bridge_member' : bridge})
+
+ # Check if we are a member of a bond device
+ bond = is_member(config, ifname, 'bonding')
+ if bond:
+ dict.update({'is_bond_member' : bond})
+
+ mac = leaf_node_changed(config, ['mac'])
+ if mac:
+ dict.update({'mac_old' : mac})
+
+ eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64'])
+ if eui64:
+ # XXX: T2636 workaround: convert string to a list with one element
+ if isinstance(eui64, str):
+ eui64 = [eui64]
+ tmp = vyos_dict_search('ipv6.address', dict)
+ if not tmp:
+ dict.update({'ipv6': {'address': {'eui64_old': eui64}}})
+ else:
+ dict['ipv6']['address'].update({'eui64_old': eui64})
+
+ # remove wrongly inserted values
+ dict = T2665_default_dict_cleanup(dict)
+
+ # Implant default dictionary for DHCPv6-PD instances
+ default_pd_values = defaults(base + ['dhcpv6-options', 'pd'])
+ dict = dict_add_dhcpv6pd_defaults(default_pd_values, dict)
+
+ # Implant default dictionary in vif/vif-s VLAN interfaces. Values are
+ # identical for all types of VLAN interfaces as they all include the same
+ # XML definitions which hold the defaults.
+ default_vif_values = defaults(base + ['vif'])
+ for vif, vif_config in dict.get('vif', {}).items():
+ dict['vif'][vif] = dict_add_dhcpv6pd_defaults(
+ default_pd_values, vif_config)
+ dict['vif'][vif] = T2665_default_dict_cleanup(
+ dict_merge(default_vif_values, vif_config))
+
+ for vif_s, vif_s_config in dict.get('vif_s', {}).items():
+ dict['vif_s'][vif_s] = dict_add_dhcpv6pd_defaults(
+ default_pd_values, vif_s_config)
+ dict['vif_s'][vif_s] = T2665_default_dict_cleanup(
+ dict_merge(default_vif_values, vif_s_config))
+ for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
+ dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_add_dhcpv6pd_defaults(
+ default_pd_values, vif_c_config)
+ dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_default_dict_cleanup(
+ dict_merge(default_vif_values, vif_c_config))
+
+ # Check vif, vif-s/vif-c VLAN interfaces for removal
+ dict = get_removed_vlans(config, dict)
+
+ return dict
+
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/configsession.py b/python/vyos/configsession.py
new file mode 100644
index 000000000..0994fd974
--- /dev/null
+++ b/python/vyos/configsession.py
@@ -0,0 +1,191 @@
+# configsession -- the write API for the VyOS running config
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+import re
+import sys
+import subprocess
+
+CLI_SHELL_API = '/bin/cli-shell-api'
+SET = '/opt/vyatta/sbin/my_set'
+DELETE = '/opt/vyatta/sbin/my_delete'
+COMMENT = '/opt/vyatta/sbin/my_comment'
+COMMIT = '/opt/vyatta/sbin/my_commit'
+DISCARD = '/opt/vyatta/sbin/my_discard'
+SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
+LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
+SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl']
+INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url']
+REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
+GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
+SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show']
+
+# Default "commit via" string
+APP = "vyos-http-api"
+
+# When started as a service rather than from a user shell,
+# the process lacks the VyOS-specific environment that comes
+# from bash configs, so we have to inject it
+# XXX: maybe it's better to do via a systemd environment file
+def inject_vyos_env(env):
+ env['VYATTA_CFG_GROUP_NAME'] = 'vyattacfg'
+ env['VYATTA_USER_LEVEL_DIR'] = '/opt/vyatta/etc/shell/level/admin'
+ env['VYATTA_PROCESS_CLIENT'] = 'gui2_rest'
+ env['VYOS_HEADLESS_CLIENT'] = 'vyos_http_api'
+ env['vyatta_bindir']= '/opt/vyatta/bin'
+ env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
+ env['vyatta_configdir'] = '/opt/vyatta/config'
+ env['vyatta_datadir'] = '/opt/vyatta/share'
+ env['vyatta_datarootdir'] = '/opt/vyatta/share'
+ env['vyatta_libdir'] = '/opt/vyatta/lib'
+ env['vyatta_libexecdir'] = '/opt/vyatta/libexec'
+ env['vyatta_op_templates'] = '/opt/vyatta/share/vyatta-op/templates'
+ env['vyatta_prefix'] = '/opt/vyatta'
+ env['vyatta_sbindir'] = '/opt/vyatta/sbin'
+ env['vyatta_sysconfdir'] = '/opt/vyatta/etc'
+ env['vyos_bin_dir'] = '/usr/bin'
+ env['vyos_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
+ env['vyos_completion_dir'] = '/usr/libexec/vyos/completion'
+ env['vyos_configdir'] = '/opt/vyatta/config'
+ env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode'
+ env['vyos_datadir'] = '/opt/vyatta/share'
+ env['vyos_datarootdir']= '/opt/vyatta/share'
+ env['vyos_libdir'] = '/opt/vyatta/lib'
+ env['vyos_libexec_dir'] = '/usr/libexec/vyos'
+ env['vyos_op_scripts_dir'] = '/usr/libexec/vyos/op_mode'
+ env['vyos_op_templates'] = '/opt/vyatta/share/vyatta-op/templates'
+ env['vyos_prefix'] = '/opt/vyatta'
+ env['vyos_sbin_dir'] = '/usr/sbin'
+ env['vyos_validators_dir'] = '/usr/libexec/vyos/validators'
+
+ return env
+
+
+class ConfigSessionError(Exception):
+ pass
+
+
+class ConfigSession(object):
+ """
+ The write API of VyOS.
+ """
+ def __init__(self, session_id, app=APP):
+ """
+ Creates a new config session.
+
+ Args:
+ session_id (str): Session identifier
+ app (str): Application name, purely informational
+
+ Note:
+ The session identifier MUST be globally unique within the system.
+ The best practice is to only have one ConfigSession object per process
+ and used the PID for the session identifier.
+ """
+
+ env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)])
+ self.__session_id = session_id
+
+ # Extract actual variables from the chunk of shell it outputs
+ # XXX: it's better to extend cli-shell-api to provide easily readable output
+ env_list = re.findall(r'([A-Z_]+)=([^;\s]+)', env_str.decode())
+
+ session_env = os.environ
+ session_env = inject_vyos_env(session_env)
+ for k, v in env_list:
+ session_env[k] = v
+
+ self.__session_env = session_env
+ self.__session_env["COMMIT_VIA"] = app
+
+ self.__run_command([CLI_SHELL_API, 'setupSession'])
+
+ def __del__(self):
+ try:
+ output = subprocess.check_output([CLI_SHELL_API, 'teardownSession'], env=self.__session_env).decode().strip()
+ if output:
+ print("cli-shell-api teardownSession output for sesion {0}: {1}".format(self.__session_id, output), file=sys.stderr)
+ except Exception as e:
+ print("Could not tear down session {0}: {1}".format(self.__session_id, e), file=sys.stderr)
+
+ def __run_command(self, cmd_list):
+ p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env)
+ result = p.wait()
+ output = p.stdout.read().decode()
+ p.communicate()
+ if result != 0:
+ raise ConfigSessionError(output)
+ return output
+
+ def get_session_env(self):
+ return self.__session_env
+
+ def set(self, path, value=None):
+ if not value:
+ value = []
+ else:
+ value = [value]
+ self.__run_command([SET] + path + value)
+
+ def delete(self, path, value=None):
+ if not value:
+ value = []
+ else:
+ value = [value]
+ self.__run_command([DELETE] + path + value)
+
+ def comment(self, path, value=None):
+ if not value:
+ value = [""]
+ else:
+ value = [value]
+ self.__run_command([COMMENT] + path + value)
+
+ def commit(self):
+ out = self.__run_command([COMMIT])
+ return out
+
+ def discard(self):
+ self.__run_command([DISCARD])
+
+ def show_config(self, path, format='raw'):
+ config_data = self.__run_command(SHOW_CONFIG + path)
+
+ if format == 'raw':
+ return config_data
+
+ def load_config(self, file_path):
+ out = self.__run_command(LOAD_CONFIG + [file_path])
+ return out
+
+ def save_config(self, file_path):
+ out = self.__run_command(SAVE_CONFIG + [file_path])
+ return out
+
+ def install_image(self, url):
+ out = self.__run_command(INSTALL_IMAGE + [url])
+ return out
+
+ def remove_image(self, name):
+ out = self.__run_command(REMOVE_IMAGE + [name])
+ return out
+
+ def generate(self, path):
+ out = self.__run_command(GENERATE + path)
+ return out
+
+ def show(self, path):
+ out = self.__run_command(SHOW + path)
+ return out
+
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/configtree.py b/python/vyos/configtree.py
new file mode 100644
index 000000000..d8ffaca99
--- /dev/null
+++ b/python/vyos/configtree.py
@@ -0,0 +1,283 @@
+# configtree -- a standalone VyOS config file manipulation library (Python bindings)
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import re
+import json
+
+from ctypes import cdll, c_char_p, c_void_p, c_int
+
+
+def escape_backslash(string: str) -> str:
+ """Escape single backslashes in string that are not in escape sequence"""
+ p = re.compile(r'(?<!\\)[\\](?!b|f|n|r|t|\\[^bfnrt])')
+ result = p.sub(r'\\\\', string)
+ return result
+
+def extract_version(s):
+ """ Extract the version string from the config string """
+ t = re.split('(^//)', s, maxsplit=1, flags=re.MULTILINE)
+ return (s, ''.join(t[1:]))
+
+def check_path(path):
+ # Necessary type checking
+ if not isinstance(path, list):
+ raise TypeError("Expected a list, got a {}".format(type(path)))
+ else:
+ pass
+
+
+class ConfigTreeError(Exception):
+ pass
+
+
+class ConfigTree(object):
+ def __init__(self, config_string, libpath='/usr/lib/libvyosconfig.so.0'):
+ self.__config = None
+ self.__lib = cdll.LoadLibrary(libpath)
+
+ # Import functions
+ self.__from_string = self.__lib.from_string
+ self.__from_string.argtypes = [c_char_p]
+ self.__from_string.restype = c_void_p
+
+ self.__get_error = self.__lib.get_error
+ self.__get_error.argtypes = []
+ self.__get_error.restype = c_char_p
+
+ self.__to_string = self.__lib.to_string
+ self.__to_string.argtypes = [c_void_p]
+ self.__to_string.restype = c_char_p
+
+ self.__to_commands = self.__lib.to_commands
+ self.__to_commands.argtypes = [c_void_p]
+ self.__to_commands.restype = c_char_p
+
+ self.__to_json = self.__lib.to_json
+ self.__to_json.argtypes = [c_void_p]
+ self.__to_json.restype = c_char_p
+
+ self.__to_json_ast = self.__lib.to_json_ast
+ self.__to_json_ast.argtypes = [c_void_p]
+ self.__to_json_ast.restype = c_char_p
+
+ self.__set_add_value = self.__lib.set_add_value
+ self.__set_add_value.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__set_add_value.restype = c_int
+
+ self.__delete_value = self.__lib.delete_value
+ self.__delete_value.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__delete_value.restype = c_int
+
+ self.__delete = self.__lib.delete_node
+ self.__delete.argtypes = [c_void_p, c_char_p]
+ self.__delete.restype = c_int
+
+ self.__rename = self.__lib.rename_node
+ self.__rename.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__rename.restype = c_int
+
+ self.__copy = self.__lib.copy_node
+ self.__copy.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__copy.restype = c_int
+
+ self.__set_replace_value = self.__lib.set_replace_value
+ self.__set_replace_value.argtypes = [c_void_p, c_char_p, c_char_p]
+ self.__set_replace_value.restype = c_int
+
+ self.__set_valueless = self.__lib.set_valueless
+ self.__set_valueless.argtypes = [c_void_p, c_char_p]
+ self.__set_valueless.restype = c_int
+
+ self.__exists = self.__lib.exists
+ self.__exists.argtypes = [c_void_p, c_char_p]
+ self.__exists.restype = c_int
+
+ self.__list_nodes = self.__lib.list_nodes
+ self.__list_nodes.argtypes = [c_void_p, c_char_p]
+ self.__list_nodes.restype = c_char_p
+
+ self.__return_value = self.__lib.return_value
+ self.__return_value.argtypes = [c_void_p, c_char_p]
+ self.__return_value.restype = c_char_p
+
+ self.__return_values = self.__lib.return_values
+ self.__return_values.argtypes = [c_void_p, c_char_p]
+ self.__return_values.restype = c_char_p
+
+ self.__is_tag = self.__lib.is_tag
+ self.__is_tag.argtypes = [c_void_p, c_char_p]
+ self.__is_tag.restype = c_int
+
+ self.__set_tag = self.__lib.set_tag
+ self.__set_tag.argtypes = [c_void_p, c_char_p]
+ self.__set_tag.restype = c_int
+
+ self.__destroy = self.__lib.destroy
+ self.__destroy.argtypes = [c_void_p]
+
+ config_section, version_section = extract_version(config_string)
+ config_section = escape_backslash(config_section)
+ config = self.__from_string(config_section.encode())
+ if config is None:
+ msg = self.__get_error().decode()
+ raise ValueError("Failed to parse config: {0}".format(msg))
+ else:
+ self.__config = config
+ self.__version = version_section
+
+ def __del__(self):
+ if self.__config is not None:
+ self.__destroy(self.__config)
+
+ def __str__(self):
+ return self.to_string()
+
+ def to_string(self):
+ config_string = self.__to_string(self.__config).decode()
+ config_string = "{0}\n{1}".format(config_string, self.__version)
+ return config_string
+
+ def to_commands(self):
+ return self.__to_commands(self.__config).decode()
+
+ def to_json(self):
+ return self.__to_json(self.__config).decode()
+
+ def to_json_ast(self):
+ return self.__to_json_ast(self.__config).decode()
+
+ def set(self, path, value=None, replace=True):
+ """Set new entry in VyOS configuration.
+ path: configuration path e.g. 'system dns forwarding listen-address'
+ value: value to be added to node, e.g. '172.18.254.201'
+ replace: True: current occurance will be replaced
+ False: new value will be appended to current occurances - use
+ this for adding values to a multi node
+ """
+
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ if value is None:
+ self.__set_valueless(self.__config, path_str)
+ else:
+ if replace:
+ self.__set_replace_value(self.__config, path_str, str(value).encode())
+ else:
+ self.__set_add_value(self.__config, path_str, str(value).encode())
+
+ def delete(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ self.__delete(self.__config, path_str)
+
+ def delete_value(self, path, value):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ self.__delete_value(self.__config, path_str, value.encode())
+
+ def rename(self, path, new_name):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+ newname_str = new_name.encode()
+
+ # Check if a node with intended new name already exists
+ new_path = path[:-1] + [new_name]
+ if self.exists(new_path):
+ raise ConfigTreeError()
+ res = self.__rename(self.__config, path_str, newname_str)
+ if (res != 0):
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path))
+
+ def copy(self, old_path, new_path):
+ check_path(old_path)
+ check_path(new_path)
+ oldpath_str = " ".join(map(str, old_path)).encode()
+ newpath_str = " ".join(map(str, new_path)).encode()
+
+ # Check if a node with intended new name already exists
+ if self.exists(new_path):
+ raise ConfigTreeError()
+ res = self.__copy(self.__config, oldpath_str, newpath_str)
+ if (res != 0):
+ raise ConfigTreeError("Path [{}] doesn't exist".format(old_path))
+
+ def exists(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res = self.__exists(self.__config, path_str)
+ if (res == 0):
+ return False
+ else:
+ return True
+
+ def list_nodes(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res_json = self.__list_nodes(self.__config, path_str).decode()
+ res = json.loads(res_json)
+
+ if res is None:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return res
+
+ def return_value(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res_json = self.__return_value(self.__config, path_str).decode()
+ res = json.loads(res_json)
+
+ if res is None:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return res
+
+ def return_values(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res_json = self.__return_values(self.__config, path_str).decode()
+ res = json.loads(res_json)
+
+ if res is None:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return res
+
+ def is_tag(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res = self.__is_tag(self.__config, path_str)
+ if (res >= 1):
+ return True
+ else:
+ return False
+
+ def set_tag(self, path):
+ check_path(path)
+ path_str = " ".join(map(str, path)).encode()
+
+ res = self.__set_tag(self.__config, path_str)
+ if (res == 0):
+ return True
+ else:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
new file mode 100644
index 000000000..7e1930878
--- /dev/null
+++ b/python/vyos/configverify.py
@@ -0,0 +1,139 @@
+# 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:
+ if config['vrf'] not in interfaces():
+ raise ConfigError('VRF "{vrf}" does not exist'.format(**config))
+
+ if 'is_bridge_member' in config:
+ 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 assignment when interface is part
+ of a bridge or bond.
+ """
+ if {'is_bridge_member', 'address'} <= set(config):
+ raise ConfigError(
+ 'Cannot assign address to interface "{ifname}" as it is a '
+ '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:
+ raise ConfigError(
+ 'Interface "{ifname}" cannot be deleted as it is a '
+ 'member of bridge "{is_bridge_member}"!'.format(**config))
+
+def verify_interface_exists(config):
+ """
+ Common helper function used by interface implementations to perform
+ recurring validation if an interface actually exists.
+ """
+ from netifaces import interfaces
+ if not config['ifname'] in interfaces():
+ raise ConfigError('Interface "{ifname}" does not exist!'
+ .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 'source_interface' not in config:
+ raise ConfigError('Physical source-interface required for '
+ 'interface "{ifname}"'.format(**config))
+ if config['source_interface'] not in interfaces():
+ raise ConfigError('Source interface {source_interface} does not '
+ 'exist'.format(**config))
+
+def verify_dhcpv6(config):
+ """
+ Common helper function used by interface implementations to perform
+ recurring validation of DHCPv6 options which are mutually exclusive.
+ """
+ if 'dhcpv6_options' in config:
+ from vyos.util import vyos_dict_search
+
+ if {'parameters_only', 'temporary'} <= set(config['dhcpv6_options']):
+ raise ConfigError('DHCPv6 temporary and parameters-only options '
+ 'are mutually exclusive!')
+
+ # It is not allowed to have duplicate SLA-IDs as those identify an
+ # assigned IPv6 subnet from a delegated prefix
+ for pd in vyos_dict_search('dhcpv6_options.pd', config):
+ sla_ids = []
+ for interface in vyos_dict_search(f'dhcpv6_options.pd.{pd}.interface', config):
+ sla_id = vyos_dict_search(
+ f'dhcpv6_options.pd.{pd}.interface.{interface}.sla_id', config)
+ sla_ids.append(sla_id)
+
+ # Check for duplicates
+ duplicates = [x for n, x in enumerate(sla_ids) if x in sla_ids[:n]]
+ if duplicates:
+ raise ConfigError('Site-Level Aggregation Identifier (SLA-ID) '
+ 'must be unique per prefix-delegation!')
+
+def verify_vlan_config(config):
+ """
+ Common helper function used by interface implementations to perform
+ recurring validation of interface VLANs
+ """
+ # 802.1q VLANs
+ for vlan in config.get('vif', {}):
+ vlan = config['vif'][vlan]
+ verify_dhcpv6(vlan)
+ verify_address(vlan)
+ verify_vrf(vlan)
+
+ # 802.1ad (Q-in-Q) VLANs
+ for vlan in config.get('vif_s', {}):
+ vlan = config['vif_s'][vlan]
+ verify_dhcpv6(vlan)
+ verify_address(vlan)
+ verify_vrf(vlan)
+
+ for vlan in config.get('vif_s', {}).get('vif_c', {}):
+ vlan = config['vif_c'][vlan]
+ verify_dhcpv6(vlan)
+ verify_address(vlan)
+ verify_vrf(vlan)
diff --git a/python/vyos/debug.py b/python/vyos/debug.py
new file mode 100644
index 000000000..6ce42b173
--- /dev/null
+++ b/python/vyos/debug.py
@@ -0,0 +1,205 @@
+# Copyright 2019 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 sys
+from datetime import datetime
+
+def message(message, flag='', destination=sys.stdout):
+ """
+ print a debug message line on stdout if debugging is enabled for the flag
+ also log it to a file if the flag 'log' is enabled
+
+ message: the message to print
+ flag: which flag must be set for it to print
+ destination: which file like object to write to (default: sys.stdout)
+
+ returns if any message was logged or not
+ """
+ enable = enabled(flag)
+ if enable:
+ destination.write(_format(flag,message))
+
+ # the log flag is special as it logs all the commands
+ # executed to a log
+ logfile = _logfile('log', '/tmp/developer-log')
+ if not logfile:
+ return enable
+
+ try:
+ # at boot the file is created as root:vyattacfg
+ # at runtime the file is created as user:vyattacfg
+ # but the helper scripts are not run as this so it
+ # need the default permission to be 666 (an not 660)
+ mask = os.umask(0o111)
+
+ with open(logfile, 'a') as f:
+ f.write(_timed(_format('log', message)))
+ finally:
+ os.umask(mask)
+
+ return enable
+
+
+def enabled(flag):
+ """
+ a flag can be set by touching the file in /tmp or /config
+
+ The current flags are:
+ - developer: the code will drop into PBD on un-handled exception
+ - log: the code will log all command to a file
+ - ifconfig: when modifying an interface,
+ prints command with result and sysfs access on stdout for interface
+ - command: print command run with result
+
+ Having the flag setup on the filesystem is required to have
+ debuging at boot time, however, setting the flag via environment
+ does not require a seek to the filesystem and is more efficient
+ it can be done on the shell on via .bashrc for the user
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns either the file or environment name used to set it up
+ """
+
+ # this is to force all new flags to be registered here to be
+ # documented both here and a reminder to update readthedocs :-)
+ if flag not in ['developer', 'log', 'ifconfig', 'command']:
+ return ''
+
+ return _fromenv(flag) or _fromfile(flag)
+
+
+def _timed(message):
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ return f'{now} {message}'
+
+
+def _remove_invisible(string):
+ for char in ('\0', '\a', '\b', '\f', '\v'):
+ string = string.replace(char, '')
+ return string
+
+
+def _format(flag, message):
+ """
+ format a log message
+ """
+ message = _remove_invisible(message)
+ return f'DEBUG/{flag.upper():<7} {message}\n'
+
+
+def _fromenv(flag):
+ """
+ check if debugging is set for this flag via environment
+
+ For a given debug flag named "test"
+ The presence of the environment VYOS_TEST_DEBUG (uppercase) enables it
+
+ return empty string if not
+ return content of env value it is
+ """
+
+ flagname = f'VYOS_{flag.upper()}_DEBUG'
+ flagenv = os.environ.get(flagname, None)
+
+ if flagenv is None:
+ return ''
+ return flagenv
+
+
+def _fromfile(flag):
+ """
+ Check if debug exist for a given debug flag name
+
+ Check is a debug flag was set by the user. the flag can be set either:
+ - in /tmp for a non-persistent presence between reboot
+ - in /config for always on (an existence at boot time)
+
+ For a given debug flag named "test"
+ The presence of the file vyos.test.debug (all lowercase) enables it
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns the full flagname
+ """
+
+ for folder in ('/tmp', '/config'):
+ flagfile = f'{folder}/vyos.{flag}.debug'
+ if os.path.isfile(flagfile):
+ return flagfile
+
+ return ''
+
+
+def _contentenv(flag):
+ return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip()
+
+
+def _contentfile(flag, default=''):
+ """
+ Check if debug exist for a given debug flag name
+
+ Check is a debug flag was set by the user. the flag can be set either:
+ - in /tmp for a non-persistent presence between reboot
+ - in /config for always on (an existence at boot time)
+
+ For a given debug flag named "test"
+ The presence of the file vyos.test.debug (all lowercase) enables it
+
+ The function returns an empty string if the flag was not set otherwise
+ the function returns the full flagname
+ """
+
+ for folder in ('/tmp', '/config'):
+ flagfile = f'{folder}/vyos.{flag}.debug'
+ if not os.path.isfile(flagfile):
+ continue
+ with open(flagfile) as f:
+ content = f.readline().strip()
+ return content or default
+
+ return ''
+
+
+def _logfile(flag, default):
+ """
+ return the name of the file to use for logging when the flag 'log' is set
+ if it could not be established or the location is invalid it returns
+ an empty string
+ """
+
+ # For log we return the location of the log file
+ log_location = _contentenv(flag) or _contentfile(flag, default)
+
+ # it was not set
+ if not log_location:
+ return ''
+
+ # Make sure that the logs can only be in /tmp, /var/log, or /tmp
+ if not log_location.startswith('/tmp/') and \
+ not log_location.startswith('/config/') and \
+ not log_location.startswith('/var/log/'):
+ return default
+ # Do not allow to escape the folders
+ if '..' in log_location:
+ return default
+
+ if not os.path.exists(log_location):
+ return log_location
+
+ # this permission is unique the the config and var folder
+ stat = os.stat(log_location).st_mode
+ if stat != 0o100666:
+ return default
+ return log_location
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
new file mode 100644
index 000000000..9921e3b5f
--- /dev/null
+++ b/python/vyos/defaults.py
@@ -0,0 +1,53 @@
+# Copyright 2018 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/>.
+
+
+directories = {
+ "data": "/usr/share/vyos/",
+ "conf_mode": "/usr/libexec/vyos/conf_mode",
+ "config": "/opt/vyatta/etc/config",
+ "current": "/opt/vyatta/etc/config-migrate/current",
+ "migrate": "/opt/vyatta/etc/config-migrate/migrate",
+ "log": "/var/log/vyatta",
+ "templates": "/usr/share/vyos/templates/",
+ "certbot": "/config/auth/letsencrypt"
+}
+
+cfg_group = 'vyattacfg'
+
+cfg_vintage = 'vyos'
+
+commit_lock = '/opt/vyatta/config/.lock'
+
+version_file = '/usr/share/vyos/component-versions.json'
+
+https_data = {
+ 'listen_addresses' : { '*': ['_'] }
+}
+
+api_data = {
+ 'listen_address' : '127.0.0.1',
+ 'port' : '8080',
+ 'strict' : 'false',
+ 'debug' : 'false',
+ 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ]
+}
+
+vyos_cert_data = {
+ "conf": "/etc/nginx/snippets/vyos-cert.conf",
+ "crt": "/etc/ssl/certs/vyos-selfsigned.crt",
+ "key": "/etc/ssl/private/vyos-selfsign",
+ "lifetime": "365",
+}
diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py
new file mode 100644
index 000000000..b12cda40f
--- /dev/null
+++ b/python/vyos/dicts.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from vyos import ConfigError
+
+
+class FixedDict(dict):
+ """
+ FixedDict: A dictionnary not allowing new keys to be created after initialisation.
+
+ >>> f = FixedDict(**{'count':1})
+ >>> f['count'] = 2
+ >>> f['king'] = 3
+ File "...", line ..., in __setitem__
+ raise ConfigError(f'Option "{k}" has no defined default')
+ """
+
+ def __init__(self, **options):
+ self._allowed = options.keys()
+ super().__init__(**options)
+
+ def __setitem__(self, k, v):
+ """
+ __setitem__ is a builtin which is called by python when setting dict values:
+ >>> d = dict()
+ >>> d['key'] = 'value'
+ >>> d
+ {'key': 'value'}
+
+ is syntaxic sugar for
+
+ >>> d = dict()
+ >>> d.__setitem__('key','value')
+ >>> d
+ {'key': 'value'}
+ """
+ if k not in self._allowed:
+ raise ConfigError(f'Option "{k}" has no defined default')
+ super().__setitem__(k, v)
diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py
new file mode 100644
index 000000000..29117a5d3
--- /dev/null
+++ b/python/vyos/formatversions.py
@@ -0,0 +1,109 @@
+# Copyright 2019 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 sys
+import os
+import re
+import fileinput
+
+def read_vyatta_versions(config_file):
+ config_file_versions = {}
+
+ with open(config_file, 'r') as config_file_handle:
+ for config_line in config_file_handle:
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line):
+ if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line):
+ raise ValueError("malformed configuration string: "
+ "{}".format(config_line))
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
+ config_file_versions[pair[0]] = int(pair[1])
+
+
+ return config_file_versions
+
+def read_vyos_versions(config_file):
+ config_file_versions = {}
+
+ with open(config_file, 'r') as config_file_handle:
+ for config_line in config_file_handle:
+ if re.match(r'// vyos-config-version:.+', config_line):
+ if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line):
+ raise ValueError("malformed configuration string: "
+ "{}".format(config_line))
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
+ config_file_versions[pair[0]] = int(pair[1])
+
+ return config_file_versions
+
+def remove_versions(config_file):
+ """
+ Remove old version string.
+ """
+ for line in fileinput.input(config_file, inplace=True):
+ if re.match(r'/\* Warning:.+ \*/$', line):
+ continue
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
+ continue
+ if re.match(r'/\* Release version:.+ \*/$', line):
+ continue
+ if re.match('// vyos-config-version:.+', line):
+ continue
+ if re.match('// Warning:.+', line):
+ continue
+ if re.match('// Release version:.+', line):
+ continue
+ sys.stdout.write(line)
+
+def format_versions_string(config_versions):
+ cfg_keys = list(config_versions.keys())
+ cfg_keys.sort()
+
+ component_version_strings = []
+
+ for key in cfg_keys:
+ cfg_vers = config_versions[key]
+ component_version_strings.append('{}@{}'.format(key, cfg_vers))
+
+ separator = ":"
+ component_version_string = separator.join(component_version_strings)
+
+ return component_version_string
+
+def write_vyatta_versions_foot(config_file, component_version_string,
+ os_version_string):
+ if config_file:
+ with open(config_file, 'a') as config_file_handle:
+ config_file_handle.write('/* Warning: Do not remove the following line. */\n')
+ config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
+ config_file_handle.write('/* Release version: {} */\n'.format(os_version_string))
+ else:
+ sys.stdout.write('/* Warning: Do not remove the following line. */\n')
+ sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
+ sys.stdout.write('/* Release version: {} */\n'.format(os_version_string))
+
+def write_vyos_versions_foot(config_file, component_version_string,
+ os_version_string):
+ if config_file:
+ with open(config_file, 'a') as config_file_handle:
+ config_file_handle.write('// Warning: Do not remove the following line.\n')
+ config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string))
+ config_file_handle.write('// Release version: {}\n'.format(os_version_string))
+ else:
+ sys.stdout.write('// Warning: Do not remove the following line.\n')
+ sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string))
+ sys.stdout.write('// Release version: {}\n'.format(os_version_string))
+
diff --git a/python/vyos/frr.py b/python/vyos/frr.py
new file mode 100644
index 000000000..3fc75bbdf
--- /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/hostsd_client.py b/python/vyos/hostsd_client.py
new file mode 100644
index 000000000..303b6ea47
--- /dev/null
+++ b/python/vyos/hostsd_client.py
@@ -0,0 +1,119 @@
+import json
+import zmq
+
+SOCKET_PATH = "ipc:///run/vyos-hostsd/vyos-hostsd.sock"
+
+class VyOSHostsdError(Exception):
+ pass
+
+class Client(object):
+ def __init__(self):
+ try:
+ context = zmq.Context()
+ self.__socket = context.socket(zmq.REQ)
+ self.__socket.RCVTIMEO = 10000 #ms
+ self.__socket.setsockopt(zmq.LINGER, 0)
+ self.__socket.connect(SOCKET_PATH)
+ except zmq.error.Again:
+ raise VyOSHostsdError("Could not connect to vyos-hostsd")
+
+ def _communicate(self, msg):
+ try:
+ request = json.dumps(msg).encode()
+ self.__socket.send(request)
+
+ reply_msg = self.__socket.recv().decode()
+ reply = json.loads(reply_msg)
+ if 'error' in reply:
+ raise VyOSHostsdError(reply['error'])
+ else:
+ return reply["data"]
+ except zmq.error.Again:
+ raise VyOSHostsdError("Could not connect to vyos-hostsd")
+
+ def add_name_servers(self, data):
+ msg = {'type': 'name_servers', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_name_servers(self, data):
+ msg = {'type': 'name_servers', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_name_servers(self, tag_regex):
+ msg = {'type': 'name_servers', 'op': 'get', 'tag_regex': tag_regex}
+ return self._communicate(msg)
+
+ def add_name_server_tags_recursor(self, data):
+ msg = {'type': 'name_server_tags_recursor', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_name_server_tags_recursor(self, data):
+ msg = {'type': 'name_server_tags_recursor', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_name_server_tags_recursor(self):
+ msg = {'type': 'name_server_tags_recursor', 'op': 'get'}
+ return self._communicate(msg)
+
+ def add_name_server_tags_system(self, data):
+ msg = {'type': 'name_server_tags_system', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_name_server_tags_system(self, data):
+ msg = {'type': 'name_server_tags_system', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_name_server_tags_system(self):
+ msg = {'type': 'name_server_tags_system', 'op': 'get'}
+ return self._communicate(msg)
+
+ def add_forward_zones(self, data):
+ msg = {'type': 'forward_zones', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_forward_zones(self, data):
+ msg = {'type': 'forward_zones', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_forward_zones(self):
+ msg = {'type': 'forward_zones', 'op': 'get'}
+ return self._communicate(msg)
+
+ def add_search_domains(self, data):
+ msg = {'type': 'search_domains', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_search_domains(self, data):
+ msg = {'type': 'search_domains', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_search_domains(self, tag_regex):
+ msg = {'type': 'search_domains', 'op': 'get', 'tag_regex': tag_regex}
+ return self._communicate(msg)
+
+ def add_hosts(self, data):
+ msg = {'type': 'hosts', 'op': 'add', 'data': data}
+ self._communicate(msg)
+
+ def delete_hosts(self, data):
+ msg = {'type': 'hosts', 'op': 'delete', 'data': data}
+ self._communicate(msg)
+
+ def get_hosts(self, tag_regex):
+ msg = {'type': 'hosts', 'op': 'get', 'tag_regex': tag_regex}
+ return self._communicate(msg)
+
+ def set_host_name(self, host_name, domain_name):
+ msg = {
+ 'type': 'host_name',
+ 'op': 'set',
+ 'data': {
+ 'host_name': host_name,
+ 'domain_name': domain_name,
+ }
+ }
+ self._communicate(msg)
+
+ def apply(self):
+ msg = {'op': 'apply'}
+ return self._communicate(msg)
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
new file mode 100644
index 000000000..9cd8d44c1
--- /dev/null
+++ b/python/vyos/ifconfig/__init__.py
@@ -0,0 +1,44 @@
+# Copyright 2019 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 vyos.ifconfig.section import Section
+from vyos.ifconfig.control import Control
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.operational import Operational
+from vyos.ifconfig.vrrp import VRRP
+
+from vyos.ifconfig.bond import BondIf
+from vyos.ifconfig.bridge import BridgeIf
+from vyos.ifconfig.dummy import DummyIf
+from vyos.ifconfig.ethernet import EthernetIf
+from vyos.ifconfig.geneve import GeneveIf
+from vyos.ifconfig.loopback import LoopbackIf
+from vyos.ifconfig.macvlan import MACVLANIf
+from vyos.ifconfig.vxlan import VXLANIf
+from vyos.ifconfig.wireguard import WireGuardIf
+from vyos.ifconfig.vtun import VTunIf
+from vyos.ifconfig.vti import VTIIf
+from vyos.ifconfig.pppoe import PPPoEIf
+from vyos.ifconfig.tunnel import GREIf
+from vyos.ifconfig.tunnel import GRETapIf
+from vyos.ifconfig.tunnel import IP6GREIf
+from vyos.ifconfig.tunnel import IPIPIf
+from vyos.ifconfig.tunnel import IPIP6If
+from vyos.ifconfig.tunnel import IP6IP6If
+from vyos.ifconfig.tunnel import SitIf
+from vyos.ifconfig.tunnel import Sit6RDIf
+from vyos.ifconfig.wireless import WiFiIf
+from vyos.ifconfig.l2tpv3 import L2TPv3If
+from vyos.ifconfig.macsec import MACsecIf
diff --git a/python/vyos/ifconfig/afi.py b/python/vyos/ifconfig/afi.py
new file mode 100644
index 000000000..fd263d220
--- /dev/null
+++ b/python/vyos/ifconfig/afi.py
@@ -0,0 +1,19 @@
+# Copyright 2019 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/>.
+
+# https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml
+
+IP4 = 1
+IP6 = 2
diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py
new file mode 100644
index 000000000..64407401b
--- /dev/null
+++ b/python/vyos/ifconfig/bond.py
@@ -0,0 +1,383 @@
+# Copyright 2019 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
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.vlan import VLAN
+
+from vyos.util import cmd
+from vyos.util import vyos_dict_search
+from vyos.validate import assert_list
+from vyos.validate import assert_positive
+
+@Interface.register
+@VLAN.enable
+class BondIf(Interface):
+ """
+ The Linux bonding driver provides a method for aggregating multiple network
+ interfaces into a single logical "bonded" interface. The behavior of the
+ bonded interfaces depends upon the mode; generally speaking, modes provide
+ either hot standby or load balancing services. Additionally, link integrity
+ monitoring may be performed.
+ """
+
+ default = {
+ 'type': 'bond',
+ }
+ definition = {
+ **Interface.definition,
+ ** {
+ 'section': 'bonding',
+ 'prefixes': ['bond', ],
+ 'broadcast': True,
+ 'bridgeable': True,
+ },
+ }
+
+ _sysfs_set = {**Interface._sysfs_set, **{
+ 'bond_hash_policy': {
+ 'validate': lambda v: assert_list(v, ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']),
+ 'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy',
+ },
+ 'bond_miimon': {
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/bonding/miimon'
+ },
+ 'bond_arp_interval': {
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/bonding/arp_interval'
+ },
+ 'bond_arp_ip_target': {
+ # XXX: no validation of the IP
+ 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target',
+ },
+ 'bond_add_port': {
+ 'location': '/sys/class/net/{ifname}/bonding/slaves',
+ },
+ 'bond_del_port': {
+ 'location': '/sys/class/net/{ifname}/bonding/slaves',
+ },
+ 'bond_primary': {
+ 'convert': lambda name: name if name else '\0',
+ 'location': '/sys/class/net/{ifname}/bonding/primary',
+ },
+ 'bond_mode': {
+ 'validate': lambda v: assert_list(v, ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']),
+ 'location': '/sys/class/net/{ifname}/bonding/mode',
+ },
+ }}
+
+ _sysfs_get = {**Interface._sysfs_get, **{
+ 'bond_arp_ip_target': {
+ 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target',
+ }
+ }}
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ >>> i.remove()
+ """
+ # when a bond member gets deleted, all members are placed in A/D state
+ # even when they are enabled inside CLI. This will make the config
+ # and system look async.
+ slave_list = []
+ for s in self.get_slaves():
+ slave = {
+ 'ifname': s,
+ 'state': Interface(s).get_admin_state()
+ }
+ slave_list.append(slave)
+
+ # remove bond master which places members in disabled state
+ super().remove()
+
+ # replicate previous interface state before bond destruction back to
+ # physical interface
+ for slave in slave_list:
+ i = Interface(slave['ifname'])
+ i.set_admin_state(slave['state'])
+
+ def set_hash_policy(self, mode):
+ """
+ Selects the transmit hash policy to use for slave selection in
+ balance-xor, 802.3ad, and tlb modes. Possible values are: layer2,
+ layer2+3, layer3+4, encap2+3, encap3+4.
+
+ The default value is layer2
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_hash_policy('layer2+3')
+ """
+ self.set_interface('bond_hash_policy', mode)
+
+ def set_arp_interval(self, interval):
+ """
+ Specifies the ARP link monitoring frequency in milliseconds.
+
+ The ARP monitor works by periodically checking the slave devices
+ to determine whether they have sent or received traffic recently
+ (the precise criteria depends upon the bonding mode, and the
+ state of the slave). Regular traffic is generated via ARP probes
+ issued for the addresses specified by the arp_ip_target option.
+
+ If ARP monitoring is used in an etherchannel compatible mode
+ (modes 0 and 2), the switch should be configured in a mode that
+ evenly distributes packets across all links. If the switch is
+ configured to distribute the packets in an XOR fashion, all
+ replies from the ARP targets will be received on the same link
+ which could cause the other team members to fail.
+
+ value of 0 disables ARP monitoring. The default value is 0.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_arp_interval('100')
+ """
+ if int(interval) == 0:
+ """
+ Specifies the MII link monitoring frequency in milliseconds.
+ This determines how often the link state of each slave is
+ inspected for link failures. A value of zero disables MII
+ link monitoring. A value of 100 is a good starting point.
+ """
+ return self.set_interface('bond_miimon', interval)
+ else:
+ return self.set_interface('bond_arp_interval', interval)
+
+ def get_arp_ip_target(self):
+ """
+ Specifies the IP addresses to use as ARP monitoring peers when
+ arp_interval is > 0. These are the targets of the ARP request sent to
+ determine the health of the link to the targets. Specify these values
+ in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
+ a comma. At least one IP address must be given for ARP monitoring to
+ function. The maximum number of targets that can be specified is 16.
+
+ The default value is no IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').get_arp_ip_target()
+ '192.0.2.1'
+ """
+ # As this function might also be called from update() of a VLAN interface
+ # we must check if the bond_arp_ip_target retrieval worked or not - as this
+ # can not be set for a bond vif interface
+ try:
+ return self.get_interface('bond_arp_ip_target')
+ except FileNotFoundError:
+ return ''
+
+ def set_arp_ip_target(self, target):
+ """
+ Specifies the IP addresses to use as ARP monitoring peers when
+ arp_interval is > 0. These are the targets of the ARP request sent to
+ determine the health of the link to the targets. Specify these values
+ in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
+ a comma. At least one IP address must be given for ARP monitoring to
+ function. The maximum number of targets that can be specified is 16.
+
+ The default value is no IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_arp_ip_target('192.0.2.1')
+ >>> BondIf('bond0').get_arp_ip_target()
+ '192.0.2.1'
+ """
+ return self.set_interface('bond_arp_ip_target', target)
+
+ def add_port(self, interface):
+ """
+ Enslave physical interface to bond.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').add_port('eth0')
+ >>> BondIf('bond0').add_port('eth1')
+ """
+
+ # From drivers/net/bonding/bond_main.c:
+ # ...
+ # bond_set_slave_link_state(new_slave,
+ # BOND_LINK_UP,
+ # BOND_SLAVE_NOTIFY_NOW);
+ # ...
+ #
+ # The kernel will ALWAYS place new bond members in "up" state regardless
+ # what the CLI will tell us!
+
+ # Physical interface must be in admin down state before they can be
+ # enslaved. If this is not the case an error will be shown:
+ # bond0: eth0 is up - this may be due to an out of date ifenslave
+ slave = Interface(interface)
+ slave_state = slave.get_admin_state()
+ if slave_state == 'up':
+ slave.set_admin_state('down')
+
+ ret = self.set_interface('bond_add_port', f'+{interface}')
+ # The kernel will ALWAYS place new bond members in "up" state regardless
+ # what the LI is configured for - thus we place the interface in its
+ # desired state
+ slave.set_admin_state(slave_state)
+ return ret
+
+ def del_port(self, interface):
+ """
+ Remove physical port from bond
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').del_port('eth1')
+ """
+ return self.set_interface('bond_del_port', f'-{interface}')
+
+ def get_slaves(self):
+ """
+ Return a list with all configured slave interfaces on this bond.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').get_slaves()
+ ['eth1', 'eth2']
+ """
+ enslaved_ifs = []
+ # retrieve real enslaved interfaces from OS kernel
+ sysfs_bond = '/sys/class/net/{}'.format(self.config['ifname'])
+ if os.path.isdir(sysfs_bond):
+ for directory in os.listdir(sysfs_bond):
+ if 'lower_' in directory:
+ enslaved_ifs.append(directory.replace('lower_', ''))
+
+ return enslaved_ifs
+
+ def set_primary(self, interface):
+ """
+ A string (eth0, eth2, etc) specifying which slave is the primary
+ device. The specified device will always be the active slave while it
+ is available. Only when the primary is off-line will alternate devices
+ be used. This is useful when one slave is preferred over another, e.g.,
+ when one slave has higher throughput than another.
+
+ The primary option is only valid for active-backup, balance-tlb and
+ balance-alb mode.
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_primary('eth2')
+ """
+ return self.set_interface('bond_primary', interface)
+
+ def set_mode(self, mode):
+ """
+ Specifies one of the bonding policies. The default is balance-rr
+ (round robin).
+
+ Possible values are: balance-rr, active-backup, balance-xor,
+ broadcast, 802.3ad, balance-tlb, balance-alb
+
+ NOTE: the bonding mode can not be changed when the bond itself has
+ slaves
+
+ Example:
+ >>> from vyos.ifconfig import BondIf
+ >>> BondIf('bond0').set_mode('802.3ad')
+ """
+ return self.set_interface('bond_mode', mode)
+
+ 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. """
+
+ # use ref-counting function to place an interface into admin down state.
+ # set_admin_state_up() must be called the same amount of times else the
+ # interface won't come up. This can/should be used to prevent link flapping
+ # when changing interface parameters require the interface to be down.
+ # We will disable it once before reconfiguration and enable it afterwards.
+ if 'shutdown_required' in config:
+ self.set_admin_state('down')
+
+ # call base class first
+ super().update(config)
+
+ # ARP monitor targets need to be synchronized between sysfs and CLI.
+ # Unfortunately an address can't be send twice to sysfs as this will
+ # result in the following exception: OSError: [Errno 22] Invalid argument.
+ #
+ # We remove ALL addresses prior to adding new ones, this will remove
+ # addresses manually added by the user too - but as we are limited to 16 adresses
+ # from the kernel side this looks valid to me. We won't run into an error
+ # when a user added manual adresses which would result in having more
+ # then 16 adresses in total.
+ arp_tgt_addr = list(map(str, self.get_arp_ip_target().split()))
+ for addr in arp_tgt_addr:
+ self.set_arp_ip_target('-' + addr)
+
+ # Add configured ARP target addresses
+ value = vyos_dict_search('arp_monitor.target', config)
+ if isinstance(value, str):
+ value = [value]
+ if value:
+ for addr in value:
+ self.set_arp_ip_target('+' + addr)
+
+ # Bonding transmit hash policy
+ value = config.get('hash_policy')
+ if value: self.set_hash_policy(value)
+
+ # Some interface options can only be changed if the interface is
+ # administratively down
+ if self.get_admin_state() == 'down':
+ # Delete bond member port(s)
+ for interface in self.get_slaves():
+ self.del_port(interface)
+
+ # Bonding policy/mode
+ value = config.get('mode')
+ if value: self.set_mode(value)
+
+ # Add (enslave) interfaces to bond
+ value = vyos_dict_search('member.interface', config)
+ if value:
+ for interface in value:
+ # if we've come here we already verified the interface
+ # does not have an addresses configured so just flush
+ # any remaining ones
+ Interface(interface).flush_addrs()
+ self.add_port(interface)
+
+ # Primary device interface - must be set after 'mode'
+ value = config.get('primary')
+ if value: self.set_primary(value)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
new file mode 100644
index 000000000..4c76fe996
--- /dev/null
+++ b/python/vyos/ifconfig/bridge.py
@@ -0,0 +1,263 @@
+# Copyright 2019 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 vyos.ifconfig.interface import Interface
+from vyos.ifconfig.stp import STP
+from vyos.validate import assert_boolean
+from vyos.validate import assert_positive
+from vyos.util import cmd
+from vyos.util import vyos_dict_search
+
+@Interface.register
+class BridgeIf(Interface):
+ """
+ A bridge is a way to connect two Ethernet segments together in a protocol
+ independent way. Packets are forwarded based on Ethernet address, rather
+ than IP address (like a router). Since forwarding is done at Layer 2, all
+ protocols can go transparently through a bridge.
+
+ The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard.
+ """
+
+ default = {
+ 'type': 'bridge',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'bridge',
+ 'prefixes': ['br', ],
+ 'broadcast': True,
+ },
+ }
+
+ _sysfs_set = {**Interface._sysfs_set, **{
+ 'ageing_time': {
+ 'validate': assert_positive,
+ 'convert': lambda t: int(t) * 100,
+ 'location': '/sys/class/net/{ifname}/bridge/ageing_time',
+ },
+ 'forward_delay': {
+ 'validate': assert_positive,
+ 'convert': lambda t: int(t) * 100,
+ 'location': '/sys/class/net/{ifname}/bridge/forward_delay',
+ },
+ 'hello_time': {
+ 'validate': assert_positive,
+ 'convert': lambda t: int(t) * 100,
+ 'location': '/sys/class/net/{ifname}/bridge/hello_time',
+ },
+ 'max_age': {
+ 'validate': assert_positive,
+ 'convert': lambda t: int(t) * 100,
+ 'location': '/sys/class/net/{ifname}/bridge/max_age',
+ },
+ 'priority': {
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/bridge/priority',
+ },
+ 'stp': {
+ 'validate': assert_boolean,
+ 'location': '/sys/class/net/{ifname}/bridge/stp_state',
+ },
+ 'multicast_querier': {
+ 'validate': assert_boolean,
+ 'location': '/sys/class/net/{ifname}/bridge/multicast_querier',
+ },
+ }}
+
+ _command_set = {**Interface._command_set, **{
+ 'add_port': {
+ 'shellcmd': 'ip link set dev {value} master {ifname}',
+ },
+ 'del_port': {
+ 'shellcmd': 'ip link set dev {value} nomaster',
+ },
+ }}
+
+
+ def set_ageing_time(self, time):
+ """
+ Set bridge interface MAC address aging time in seconds. Internal kernel
+ representation is in centiseconds. Kernel default is 300 seconds.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').ageing_time(2)
+ """
+ self.set_interface('ageing_time', time)
+
+ def set_forward_delay(self, time):
+ """
+ Set bridge forwarding delay in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').forward_delay(15)
+ """
+ self.set_interface('forward_delay', time)
+
+ def set_hello_time(self, time):
+ """
+ Set bridge hello time in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').set_hello_time(2)
+ """
+ self.set_interface('hello_time', time)
+
+ def set_max_age(self, time):
+ """
+ Set bridge max message age in seconds. Internal Kernel representation
+ is in centiseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').set_max_age(30)
+ """
+ self.set_interface('max_age', time)
+
+ def set_priority(self, priority):
+ """
+ Set bridge max aging time in seconds.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').set_priority(8192)
+ """
+ self.set_interface('priority', priority)
+
+ def set_stp(self, state):
+ """
+ Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').set_stp(1)
+ """
+ self.set_interface('stp', state)
+
+ def set_multicast_querier(self, enable):
+ """
+ Sets whether the bridge actively runs a multicast querier or not. When a
+ bridge receives a 'multicast host membership' query from another network
+ host, that host is tracked based on the time that the query was received
+ plus the multicast query interval time.
+
+ Use enable=1 to enable or enable=0 to disable
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').set_multicast_querier(1)
+ """
+ self.set_interface('multicast_querier', enable)
+
+ def add_port(self, interface):
+ """
+ Add physical interface to bridge (member port)
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').add_port('eth0')
+ >>> BridgeIf('br0').add_port('eth1')
+ """
+ return self.set_interface('add_port', interface)
+
+ def del_port(self, interface):
+ """
+ Remove member port from bridge instance.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> BridgeIf('br0').del_port('eth1')
+ """
+ return self.set_interface('del_port', interface)
+
+ 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. """
+
+ # call base class first
+ super().update(config)
+
+ # Set ageing time
+ value = config.get('aging')
+ self.set_ageing_time(value)
+
+ # set bridge forward delay
+ value = config.get('forwarding_delay')
+ self.set_forward_delay(value)
+
+ # set hello time
+ value = config.get('hello_time')
+ self.set_hello_time(value)
+
+ # set max message age
+ value = config.get('max_age')
+ self.set_max_age(value)
+
+ # set bridge priority
+ value = config.get('priority')
+ self.set_priority(value)
+
+ # enable/disable spanning tree
+ value = '1' if 'stp' in config else '0'
+ self.set_stp(value)
+
+ # enable or disable IGMP querier
+ tmp = vyos_dict_search('igmp.querier', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_multicast_querier(value)
+
+ # remove interface from bridge
+ tmp = vyos_dict_search('member.interface_remove', config)
+ if tmp:
+ for member in tmp:
+ self.del_port(member)
+
+ STPBridgeIf = STP.enable(BridgeIf)
+ tmp = vyos_dict_search('member.interface', config)
+ if tmp:
+ for interface, interface_config in tmp.items():
+ # if we've come here we already verified the interface
+ # does not have an addresses configured so just flush
+ # any remaining ones
+ Interface(interface).flush_addrs()
+ # enslave interface port to bridge
+ self.add_port(interface)
+
+ tmp = STPBridgeIf(interface)
+ # set bridge port path cost
+ value = interface_config.get('cost')
+ tmp.set_path_cost(value)
+
+ # set bridge port path priority
+ value = interface_config.get('priority')
+ tmp.set_path_priority(value)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
new file mode 100644
index 000000000..a6fc8ac6c
--- /dev/null
+++ b/python/vyos/ifconfig/control.py
@@ -0,0 +1,185 @@
+# Copyright 2019 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
+from inspect import signature
+from inspect import _empty
+
+from vyos import debug
+from vyos.util import popen
+from vyos.util import cmd
+from vyos.ifconfig.section import Section
+
+
+class Control(Section):
+ _command_get = {}
+ _command_set = {}
+ _signature = {}
+
+ def __init__(self, **kargs):
+ # some commands (such as operation comands - show interfaces, etc.)
+ # need to query the interface statistics. If the interface
+ # code is used and the debugging is enabled, the screen output
+ # will include both the command but also the debugging for that command
+ # to prevent this, debugging can be explicitely disabled
+
+ # if debug is not explicitely disabled the the config, enable it
+ self.debug = ''
+ if kargs.get('debug', True) and debug.enabled('ifconfig'):
+ self.debug = 'ifconfig'
+
+ def _debug_msg (self, message):
+ return debug.message(message, self.debug)
+
+ def _popen(self, command):
+ return popen(command, self.debug)
+
+ def _cmd(self, command):
+ return cmd(command, self.debug)
+
+ def _get_command(self, config, name):
+ """
+ Using the defined names, set data write to sysfs.
+ """
+ cmd = self._command_get[name]['shellcmd'].format(**config)
+ return self._command_get[name].get('format', lambda _: _)(self._cmd(cmd))
+
+ def _values(self, name, validate, value):
+ """
+ looks at the validation function "validate"
+ for the interface sysfs or command and
+ returns a dict with the right options to call it
+ """
+ if name not in self._signature:
+ self._signature[name] = signature(validate)
+
+ values = {}
+
+ for k in self._signature[name].parameters:
+ default = self._signature[name].parameters[k].default
+ if default is not _empty:
+ continue
+ if k == 'self':
+ values[k] = self
+ elif k == 'ifname':
+ values[k] = self.ifname
+ else:
+ values[k] = value
+
+ return values
+
+ def _set_command(self, config, name, value):
+ """
+ Using the defined names, set data write to sysfs.
+ """
+ # the code can pass int as int
+ value = str(value)
+
+ validate = self._command_set[name].get('validate', None)
+ if validate:
+ try:
+ validate(**self._values(name, validate, value))
+ except Exception as e:
+ raise e.__class__(f'Could not set {name}. {e}')
+
+ convert = self._command_set[name].get('convert', None)
+ if convert:
+ value = convert(value)
+
+ possible = self._command_set[name].get('possible', None)
+ if possible and not possible(config['ifname'], value):
+ return False
+
+ config = {**config, **{'value': value}}
+
+ cmd = self._command_set[name]['shellcmd'].format(**config)
+ return self._command_set[name].get('format', lambda _: _)(self._cmd(cmd))
+
+ _sysfs_get = {}
+ _sysfs_set = {}
+
+ def _read_sysfs(self, filename):
+ """
+ Provide a single primitive w/ error checking for reading from sysfs.
+ """
+ value = None
+ with open(filename, 'r') as f:
+ value = f.read().rstrip('\n')
+
+ self._debug_msg("read '{}' < '{}'".format(value, filename))
+ return value
+
+ def _write_sysfs(self, filename, value):
+ """
+ Provide a single primitive w/ error checking for writing to sysfs.
+ """
+ self._debug_msg("write '{}' > '{}'".format(value, filename))
+ if os.path.isfile(filename):
+ with open(filename, 'w') as f:
+ f.write(str(value))
+ return True
+ return False
+
+ def _get_sysfs(self, config, name):
+ """
+ Using the defined names, get data write from sysfs.
+ """
+ filename = self._sysfs_get[name]['location'].format(**config)
+ if not filename:
+ return None
+ return self._read_sysfs(filename)
+
+ def _set_sysfs(self, config, name, value):
+ """
+ Using the defined names, set data write to sysfs.
+ """
+ # the code can pass int as int
+ value = str(value)
+
+ validate = self._sysfs_set[name].get('validate', None)
+ if validate:
+ try:
+ validate(**self._values(name, validate, value))
+ except Exception as e:
+ raise e.__class__(f'Could not set {name}. {e}')
+
+ config = {**config, **{'value': value}}
+
+ convert = self._sysfs_set[name].get('convert', None)
+ if convert:
+ value = convert(value)
+
+ commited = self._write_sysfs(
+ self._sysfs_set[name]['location'].format(**config), value)
+ if not commited:
+ errmsg = self._sysfs_set.get('errormsg', '')
+ if errmsg:
+ raise TypeError(errmsg.format(**config))
+ return commited
+
+ def get_interface(self, name):
+ if name in self._sysfs_get:
+ return self._get_sysfs(self.config, name)
+ if name in self._command_get:
+ return self._get_command(self.config, name)
+ raise KeyError(f'{name} is not a attribute of the interface we can get')
+
+ def set_interface(self, name, value):
+ if name in self._sysfs_set:
+ return self._set_sysfs(self.config, name, value)
+ if name in self._command_set:
+ return self._set_command(self.config, name, value)
+ raise KeyError(f'{name} is not a attribute of the interface we can set')
diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py
new file mode 100644
index 000000000..43614cd1c
--- /dev/null
+++ b/python/vyos/ifconfig/dummy.py
@@ -0,0 +1,56 @@
+# Copyright 2019 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 vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class DummyIf(Interface):
+ """
+ A dummy interface is entirely virtual like, for example, the loopback
+ interface. The purpose of a dummy interface is to provide a device to route
+ packets through without actually transmitting them.
+ """
+
+ default = {
+ 'type': 'dummy',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'dummy',
+ 'prefixes': ['dum', ],
+ },
+ }
+
+ 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. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
new file mode 100644
index 000000000..17c1bd64d
--- /dev/null
+++ b/python/vyos/ifconfig/ethernet.py
@@ -0,0 +1,309 @@
+# Copyright 2019 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
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.vlan import VLAN
+from vyos.validate import assert_list
+from vyos.util import run
+from vyos.util import vyos_dict_search
+
+@Interface.register
+@VLAN.enable
+class EthernetIf(Interface):
+ """
+ Abstraction of a Linux Ethernet Interface
+ """
+
+ default = {
+ 'type': 'ethernet',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'ethernet',
+ 'prefixes': ['lan', 'eth', 'eno', 'ens', 'enp', 'enx'],
+ 'bondable': True,
+ 'broadcast': True,
+ 'bridgeable': True,
+ 'eternal': '(lan|eth|eno|ens|enp|enx)[0-9]+$',
+ }
+ }
+
+ @staticmethod
+ def feature(ifname, option, value):
+ run(f'/sbin/ethtool -K {ifname} {option} {value}','ifconfig')
+ return False
+
+ _command_set = {**Interface._command_set, **{
+ 'gro': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'gro', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} gro {value}',
+ },
+ 'gso': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'gso', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} gso {value}',
+ },
+ 'sg': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'sg', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} sg {value}',
+ },
+ 'tso': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'tso', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} tso {value}',
+ },
+ 'ufo': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'possible': lambda i, v: EthernetIf.feature(i, 'ufo', v),
+ # 'shellcmd': '/sbin/ethtool -K {ifname} ufo {value}',
+ },
+ }}
+
+ def get_driver_name(self):
+ """
+ Return the driver name used by NIC. Some NICs don't support all
+ features e.g. changing link-speed, duplex
+
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.get_driver_name()
+ 'vmxnet3'
+ """
+ sysfs_file = '/sys/class/net/{}/device/driver/module'.format(
+ self.config['ifname'])
+ if os.path.exists(sysfs_file):
+ link = os.readlink(sysfs_file)
+ return os.path.basename(link)
+ else:
+ return None
+
+ def set_flow_control(self, enable):
+ """
+ Changes the pause parameters of the specified Ethernet device.
+
+ @param enable: true -> enable pause frames, false -> disable pause frames
+
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_flow_control(True)
+ """
+ ifname = self.config['ifname']
+
+ if enable not in ['on', 'off']:
+ raise ValueError("Value out of range")
+
+ if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']:
+ self._debug_msg('{} driver does not support changing flow control settings!'
+ .format(self.get_driver_name()))
+ return
+
+ # Get current flow control settings:
+ cmd = f'/sbin/ethtool --show-pause {ifname}'
+ output, code = self._popen(cmd)
+ if code == 76:
+ # the interface does not support it
+ return ''
+ if code:
+ # never fail here as it prevent vyos to boot
+ print(f'unexpected return code {code} from {cmd}')
+ return ''
+
+ # The above command returns - with tabs:
+ #
+ # Pause parameters for eth0:
+ # Autonegotiate: on
+ # RX: off
+ # TX: off
+ if re.search("Autonegotiate:\ton", output):
+ if enable == "on":
+ # flowcontrol is already enabled - no need to re-enable it again
+ # this will prevent the interface from flapping as applying the
+ # flow-control settings will take the interface down and bring
+ # it back up every time.
+ return ''
+
+ # Assemble command executed on system. Unfortunately there is no way
+ # to change this setting via sysfs
+ cmd = f'/sbin/ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}'
+ output, code = self._popen(cmd)
+ if code:
+ print(f'could not set flowcontrol for {ifname}')
+ return output
+
+ def set_speed_duplex(self, speed, duplex):
+ """
+ Set link speed in Mbit/s and duplex.
+
+ @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto
+ @duplex can be half, full, auto
+
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_speed_duplex('auto', 'auto')
+ """
+
+ if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', '25000', '40000', '50000', '100000', '400000']:
+ raise ValueError("Value out of range (speed)")
+
+ if duplex not in ['auto', 'full', 'half']:
+ raise ValueError("Value out of range (duplex)")
+
+ if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']:
+ self._debug_msg('{} driver does not support changing speed/duplex settings!'
+ .format(self.get_driver_name()))
+ return
+
+ # Get current speed and duplex settings:
+ cmd = '/sbin/ethtool {0}'.format(self.config['ifname'])
+ tmp = self._cmd(cmd)
+
+ if re.search("\tAuto-negotiation: on", tmp):
+ if speed == 'auto' and duplex == 'auto':
+ # bail out early as nothing is to change
+ return
+ else:
+ # read in current speed and duplex settings
+ cur_speed = 0
+ cur_duplex = ''
+ for line in tmp.splitlines():
+ if line.lstrip().startswith("Speed:"):
+ non_decimal = re.compile(r'[^\d.]+')
+ cur_speed = non_decimal.sub('', line)
+ continue
+
+ if line.lstrip().startswith("Duplex:"):
+ cur_duplex = line.split()[-1].lower()
+ break
+
+ if (cur_speed == speed) and (cur_duplex == duplex):
+ # bail out early as nothing is to change
+ return
+
+ cmd = '/sbin/ethtool -s {}'.format(self.config['ifname'])
+ if speed == 'auto' or duplex == 'auto':
+ cmd += ' autoneg on'
+ else:
+ cmd += ' speed {} duplex {} autoneg off'.format(speed, duplex)
+
+ return self._cmd(cmd)
+
+ def set_gro(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_gro('on')
+ """
+ return self.set_interface('gro', state)
+
+ def set_gso(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_gso('on')
+ """
+ return self.set_interface('gso', state)
+
+ def set_sg(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_sg('on')
+ """
+ return self.set_interface('sg', state)
+
+ def set_tso(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_tso('on')
+ """
+ return self.set_interface('tso', state)
+
+ def set_ufo(self, state):
+ """
+ Example:
+ >>> from vyos.ifconfig import EthernetIf
+ >>> i = EthernetIf('eth0')
+ >>> i.set_udp_offload('on')
+ """
+ return self.set_interface('ufo', state)
+
+
+ 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. """
+
+ # call base class first
+ super().update(config)
+
+ # disable ethernet flow control (pause frames)
+ value = 'off' if 'disable_flow_control' in config.keys() else 'on'
+ self.set_flow_control(value)
+
+ # GRO (generic receive offload)
+ tmp = vyos_dict_search('offload_options.generic_receive', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_gro(value)
+
+ # GSO (generic segmentation offload)
+ tmp = vyos_dict_search('offload_options.generic_segmentation', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_gso(value)
+
+ # scatter-gather option
+ tmp = vyos_dict_search('offload_options.scatter_gather', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_sg(value)
+
+ # TSO (TCP segmentation offloading)
+ tmp = vyos_dict_search('offload_options.udp_fragmentation', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_tso(value)
+
+ # UDP fragmentation offloading
+ tmp = vyos_dict_search('offload_options.udp_fragmentation', config)
+ value = tmp if (tmp != None) else 'off'
+ self.set_ufo(value)
+
+ # Set physical interface speed and duplex
+ if {'speed', 'duplex'} <= set(config):
+ speed = config.get('speed')
+ duplex = config.get('duplex')
+ self.set_speed_duplex(speed, duplex)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py
new file mode 100644
index 000000000..dd0658668
--- /dev/null
+++ b/python/vyos/ifconfig/geneve.py
@@ -0,0 +1,85 @@
+# Copyright 2019 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 copy import deepcopy
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class GeneveIf(Interface):
+ """
+ Geneve: Generic Network Virtualization Encapsulation
+
+ For more information please refer to:
+ https://tools.ietf.org/html/draft-gross-geneve-00
+ https://www.redhat.com/en/blog/what-geneve
+ https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve
+ https://lwn.net/Articles/644938/
+ """
+
+ default = {
+ 'type': 'geneve',
+ 'vni': 0,
+ 'remote': '',
+ }
+ options = Interface.options + \
+ ['vni', 'remote']
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'geneve',
+ 'prefixes': ['gnv', ],
+ 'bridgeable': True,
+ }
+ }
+
+ def _create(self):
+ cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote}'.format(**self.config)
+ self._cmd(cmd)
+
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
+ @classmethod
+ def get_config(cls):
+ """
+ GENEVE interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = GeneveIf().get_config()
+ """
+ return deepcopy(cls.default)
+
+ 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. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py
new file mode 100644
index 000000000..bfab36335
--- /dev/null
+++ b/python/vyos/ifconfig/input.py
@@ -0,0 +1,31 @@
+# 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 vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class InputIf(Interface):
+ default = {
+ 'type': '',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'input',
+ 'prefixes': ['ifb', ],
+ },
+ }
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
new file mode 100644
index 000000000..67ba973c4
--- /dev/null
+++ b/python/vyos/ifconfig/interface.py
@@ -0,0 +1,1067 @@
+# Copyright 2019-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 json
+import jmespath
+
+from copy import deepcopy
+from glob import glob
+
+from ipaddress import IPv4Network
+from ipaddress import IPv6Address
+from ipaddress import IPv6Network
+from netifaces import ifaddresses
+# this is not the same as socket.AF_INET/INET6
+from netifaces import AF_INET
+from netifaces import AF_INET6
+
+from vyos import ConfigError
+from vyos.configdict import list_diff
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import mac2eui64
+from vyos.util import vyos_dict_search
+from vyos.validate import is_ipv4
+from vyos.validate import is_ipv6
+from vyos.validate import is_intf_addr_assigned
+from vyos.validate import assert_boolean
+from vyos.validate import assert_list
+from vyos.validate import assert_mac
+from vyos.validate import assert_mtu
+from vyos.validate import assert_positive
+from vyos.validate import assert_range
+
+from vyos.ifconfig.control import Control
+from vyos.ifconfig.vrrp import VRRP
+from vyos.ifconfig.operational import Operational
+from vyos.ifconfig import Section
+
+def get_ethertype(ethertype_val):
+ if ethertype_val == '0x88A8':
+ return '802.1ad'
+ elif ethertype_val == '0x8100':
+ return '802.1q'
+ else:
+ raise ConfigError('invalid ethertype "{}"'.format(ethertype_val))
+
+class Interface(Control):
+ # This is the class which will be used to create
+ # self.operational, it allows subclasses, such as
+ # WireGuard to modify their display behaviour
+ OperationalClass = Operational
+
+ options = ['debug', 'create']
+ required = []
+ default = {
+ 'type': '',
+ 'debug': True,
+ 'create': True,
+ }
+ definition = {
+ 'section': '',
+ 'prefixes': [],
+ 'vlan': False,
+ 'bondable': False,
+ 'broadcast': False,
+ 'bridgeable': False,
+ 'eternal': '',
+ }
+
+ _command_get = {
+ 'admin_state': {
+ 'shellcmd': 'ip -json link show dev {ifname}',
+ 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down',
+ },
+ 'vlan_protocol': {
+ 'shellcmd': 'ip -json -details link show dev {ifname}',
+ 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)),
+ },
+ }
+
+ _command_set = {
+ 'admin_state': {
+ 'validate': lambda v: assert_list(v, ['up', 'down']),
+ 'shellcmd': 'ip link set dev {ifname} {value}',
+ },
+ 'mac': {
+ 'validate': assert_mac,
+ 'shellcmd': 'ip link set dev {ifname} address {value}',
+ },
+ 'vrf': {
+ 'convert': lambda v: f'master {v}' if v else 'nomaster',
+ 'shellcmd': 'ip link set dev {ifname} {value}',
+ },
+ }
+
+ _sysfs_get = {
+ 'alias': {
+ 'location': '/sys/class/net/{ifname}/ifalias',
+ },
+ 'mac': {
+ 'location': '/sys/class/net/{ifname}/address',
+ },
+ 'mtu': {
+ 'location': '/sys/class/net/{ifname}/mtu',
+ },
+ 'oper_state':{
+ 'location': '/sys/class/net/{ifname}/operstate',
+ },
+ }
+
+ _sysfs_set = {
+ 'alias': {
+ 'convert': lambda name: name if name else '\0',
+ 'location': '/sys/class/net/{ifname}/ifalias',
+ },
+ 'mtu': {
+ 'validate': assert_mtu,
+ 'location': '/sys/class/net/{ifname}/mtu',
+ },
+ 'arp_cache_tmo': {
+ 'convert': lambda tmo: (int(tmo) * 1000),
+ 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms',
+ },
+ 'arp_filter': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter',
+ },
+ 'arp_accept': {
+ 'validate': lambda arp: assert_range(arp,0,2),
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept',
+ },
+ 'arp_announce': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce',
+ },
+ 'arp_ignore': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore',
+ },
+ 'ipv6_accept_ra': {
+ 'validate': lambda ara: assert_range(ara,0,3),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ },
+ 'ipv6_autoconf': {
+ 'validate': lambda aco: assert_range(aco,0,2),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf',
+ },
+ 'ipv6_forwarding': {
+ 'validate': lambda fwd: assert_range(fwd,0,2),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding',
+ },
+ 'ipv6_dad_transmits': {
+ 'validate': assert_positive,
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits',
+ },
+ 'proxy_arp': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp',
+ },
+ 'proxy_arp_pvlan': {
+ 'validate': assert_boolean,
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan',
+ },
+ # link_detect vs link_filter name weirdness
+ 'link_detect': {
+ 'validate': lambda link: assert_range(link,0,3),
+ 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter',
+ },
+ }
+
+ @classmethod
+ def exists(cls, ifname):
+ return os.path.exists(f'/sys/class/net/{ifname}')
+
+ def __init__(self, ifname, **kargs):
+ """
+ This is the base interface class which supports basic IP/MAC address
+ operations as well as DHCP(v6). Other interface which represent e.g.
+ and ethernet bridge are implemented as derived classes adding all
+ additional functionality.
+
+ For creation you will need to provide the interface type, otherwise
+ the existing interface is used
+
+ DEBUG:
+ This class has embedded debugging (print) which can be enabled by
+ creating the following file:
+ vyos@vyos# touch /tmp/vyos.ifconfig.debug
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ """
+
+ self.config = deepcopy(self.default)
+ for k in self.options:
+ if k in kargs:
+ self.config[k] = kargs[k]
+
+ # make sure the ifname is the first argument and not from the dict
+ self.config['ifname'] = ifname
+ self._admin_state_down_cnt = 0
+
+ # we must have updated config before initialising the Interface
+ super().__init__(**kargs)
+ self.ifname = ifname
+
+ if not self.exists(ifname):
+ # Any instance of Interface, such as Interface('eth0')
+ # can be used safely to access the generic function in this class
+ # as 'type' is unset, the class can not be created
+ if not self.config['type']:
+ raise Exception(f'interface "{ifname}" not found')
+
+ # Should an Instance of a child class (EthernetIf, DummyIf, ..)
+ # be required, then create should be set to False to not accidentally create it.
+ # In case a subclass does not define it, we use get to set the default to True
+ if self.config.get('create',True):
+ for k in self.required:
+ if k not in kargs:
+ name = self.default['type']
+ raise ConfigError(f'missing required option {k} for {name} {ifname} creation')
+
+ self._create()
+ # If we can not connect to the interface then let the caller know
+ # as the class could not be correctly initialised
+ else:
+ raise Exception('interface "{}" not found'.format(self.config['ifname']))
+
+ # temporary list of assigned IP addresses
+ self._addr = []
+
+ self.operational = self.OperationalClass(ifname)
+ self.vrrp = VRRP(ifname)
+
+ def _create(self):
+ cmd = 'ip link add dev {ifname} type {type}'.format(**self.config)
+ self._cmd(cmd)
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ >>> i.remove()
+ """
+
+ # remove all assigned IP addresses from interface - this is a bit redundant
+ # as the kernel will remove all addresses on interface deletion, but we
+ # can not delete ALL interfaces, see below
+ self.flush_addrs()
+
+ # ---------------------------------------------------------------------
+ # Any class can define an eternal regex in its definition
+ # interface matching the regex will not be deleted
+
+ eternal = self.definition['eternal']
+ if not eternal:
+ self._delete()
+ elif not re.match(eternal, self.ifname):
+ self._delete()
+
+ def _delete(self):
+ # NOTE (Improvement):
+ # after interface removal no other commands should be allowed
+ # to be called and instead should raise an Exception:
+ cmd = 'ip link del dev {ifname}'.format(**self.config)
+ return self._cmd(cmd)
+
+ def get_mtu(self):
+ """
+ Get/set interface mtu in bytes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_mtu()
+ '1500'
+ """
+ return self.get_interface('mtu')
+
+ def set_mtu(self, mtu):
+ """
+ Get/set interface mtu in bytes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_mtu(1400)
+ >>> Interface('eth0').get_mtu()
+ '1400'
+ """
+ return self.set_interface('mtu', mtu)
+
+ def get_mac(self):
+ """
+ Get current interface MAC (Media Access Contrl) address used.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_mac()
+ '00:50:ab:cd:ef:00'
+ """
+ return self.get_interface('mac')
+
+ def set_mac(self, mac):
+ """
+ Set interface MAC (Media Access Contrl) address to given value.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01')
+ """
+
+ # If MAC is unchanged, bail out early
+ if mac == self.get_mac():
+ return None
+
+ # MAC address can only be changed if interface is in 'down' state
+ prev_state = self.get_admin_state()
+ if prev_state == 'up':
+ 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.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_vrf('foo')
+ >>> Interface('eth0').set_vrf()
+ """
+ self.set_interface('vrf', vrf)
+
+ def set_arp_cache_tmo(self, tmo):
+ """
+ Set ARP cache timeout value in seconds. Internal Kernel representation
+ is in milliseconds.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_arp_cache_tmo(40)
+ """
+ return self.set_interface('arp_cache_tmo', tmo)
+
+ def set_arp_filter(self, arp_filter):
+ """
+ Filter ARP requests
+
+ 1 - Allows you to have multiple network interfaces on the same
+ subnet, and have the ARPs for each interface be answered
+ based on whether or not the kernel would route a packet from
+ the ARP'd IP out that interface (therefore you must use source
+ based routing for this to work). In other words it allows control
+ of which cards (usually 1) will respond to an arp request.
+
+ 0 - (default) The kernel can respond to arp requests with addresses
+ from other interfaces. This may seem wrong but it usually makes
+ sense, because it increases the chance of successful communication.
+ IP addresses are owned by the complete host on Linux, not by
+ particular interfaces. Only for more complex setups like load-
+ balancing, does this behaviour cause problems.
+ """
+ return self.set_interface('arp_filter', arp_filter)
+
+ def set_arp_accept(self, arp_accept):
+ """
+ Define behavior for gratuitous ARP frames who's IP is not
+ already present in the ARP table:
+ 0 - don't create new entries in the ARP table
+ 1 - create new entries in the ARP table
+
+ Both replies and requests type gratuitous arp will trigger the
+ ARP table to be updated, if this setting is on.
+
+ If the ARP table already contains the IP address of the
+ gratuitous arp frame, the arp table will be updated regardless
+ if this setting is on or off.
+ """
+ return self.set_interface('arp_accept', arp_accept)
+
+ def set_arp_announce(self, arp_announce):
+ """
+ Define different restriction levels for announcing the local
+ source IP address from IP packets in ARP requests sent on
+ interface:
+ 0 - (default) Use any local address, configured on any interface
+ 1 - Try to avoid local addresses that are not in the target's
+ subnet for this interface. This mode is useful when target
+ hosts reachable via this interface require the source IP
+ address in ARP requests to be part of their logical network
+ configured on the receiving interface. When we generate the
+ request we will check all our subnets that include the
+ target IP and will preserve the source address if it is from
+ such subnet.
+
+ Increasing the restriction level gives more chance for
+ receiving answer from the resolved target while decreasing
+ the level announces more valid sender's information.
+ """
+ return self.set_interface('arp_announce', arp_announce)
+
+ def set_arp_ignore(self, arp_ignore):
+ """
+ Define different modes for sending replies in response to received ARP
+ requests that resolve local target IP addresses:
+
+ 0 - (default): reply for any local target IP address, configured
+ on any interface
+ 1 - reply only if the target IP address is local address
+ configured on the incoming interface
+ """
+ return self.set_interface('arp_ignore', arp_ignore)
+
+ def set_ipv6_accept_ra(self, accept_ra):
+ """
+ Accept Router Advertisements; autoconfigure using them.
+
+ It also determines whether or not to transmit Router Solicitations.
+ If and only if the functional setting is to accept Router
+ Advertisements, Router Solicitations will be transmitted.
+
+ 0 - Do not accept Router Advertisements.
+ 1 - (default) Accept Router Advertisements if forwarding is disabled.
+ 2 - Overrule forwarding behaviour. Accept Router Advertisements even if
+ forwarding is enabled.
+ """
+ return self.set_interface('ipv6_accept_ra', accept_ra)
+
+ def set_ipv6_autoconf(self, autoconf):
+ """
+ Autoconfigure addresses using Prefix Information in Router
+ Advertisements.
+ """
+ return self.set_interface('ipv6_autoconf', autoconf)
+
+ def add_ipv6_eui64_address(self, prefix):
+ """
+ Extended Unique Identifier (EUI), as per RFC2373, allows a host to
+ assign itself a unique IPv6 address based on a given IPv6 prefix.
+
+ Calculate the EUI64 from the interface's MAC, then assign it
+ with the given prefix to the interface.
+ """
+
+ eui64 = mac2eui64(self.get_mac(), prefix)
+ prefixlen = prefix.split('/')[1]
+ self.add_addr(f'{eui64}/{prefixlen}')
+
+ def del_ipv6_eui64_address(self, prefix):
+ """
+ Delete the address based on the interface's MAC-based EUI64
+ combined with the prefix address.
+ """
+ eui64 = mac2eui64(self.get_mac(), prefix)
+ prefixlen = prefix.split('/')[1]
+ self.del_addr(f'{eui64}/{prefixlen}')
+
+
+ def set_ipv6_forwarding(self, forwarding):
+ """
+ Configure IPv6 interface-specific Host/Router behaviour.
+
+ False:
+
+ By default, Host behaviour is assumed. This means:
+
+ 1. IsRouter flag is not set in Neighbour Advertisements.
+ 2. If accept_ra is TRUE (default), transmit Router
+ Solicitations.
+ 3. If accept_ra is TRUE (default), accept Router
+ Advertisements (and do autoconfiguration).
+ 4. If accept_redirects is TRUE (default), accept Redirects.
+
+ True:
+
+ If local forwarding is enabled, Router behaviour is assumed.
+ This means exactly the reverse from the above:
+
+ 1. IsRouter flag is set in Neighbour Advertisements.
+ 2. Router Solicitations are not sent unless accept_ra is 2.
+ 3. Router Advertisements are ignored unless accept_ra is 2.
+ 4. Redirects are ignored.
+ """
+ return self.set_interface('ipv6_forwarding', forwarding)
+
+ def set_ipv6_dad_messages(self, dad):
+ """
+ The amount of Duplicate Address Detection probes to send.
+ Default: 1
+ """
+ return self.set_interface('ipv6_dad_transmits', dad)
+
+ def set_link_detect(self, link_filter):
+ """
+ Configure kernel response in packets received on interfaces that are 'down'
+
+ 0 - Allow packets to be received for the address on this interface
+ even if interface is disabled or no carrier.
+
+ 1 - Ignore packets received if interface associated with the incoming
+ address is down.
+
+ 2 - Ignore packets received if interface associated with the incoming
+ address is down or has no carrier.
+
+ Default value is 0. Note that some distributions enable it in startup
+ scripts.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_link_detect(1)
+ """
+ return self.set_interface('link_detect', link_filter)
+
+ def get_alias(self):
+ """
+ Get interface alias name used by e.g. SNMP
+
+ Example:
+ >>> Interface('eth0').get_alias()
+ 'interface description as set by user'
+ """
+ return self.get_interface('alias')
+
+ def set_alias(self, ifalias=''):
+ """
+ Set interface alias name used by e.g. SNMP
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_alias('VyOS upstream interface')
+
+ to clear alias e.g. delete it use:
+
+ >>> Interface('eth0').set_ifalias('')
+ """
+ self.set_interface('alias', ifalias)
+
+ def get_vlan_protocol(self):
+ """
+ Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0.10').get_vlan_protocol()
+ '802.1Q'
+ """
+ return self.get_interface('vlan_protocol')
+
+ def get_admin_state(self):
+ """
+ Get interface administrative state. Function will return 'up' or 'down'
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_admin_state()
+ 'up'
+ """
+ return self.get_interface('admin_state')
+
+ def set_admin_state(self, state):
+ """
+ Set interface administrative state to be 'up' or 'down'
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_admin_state('down')
+ >>> Interface('eth0').get_admin_state()
+ 'down'
+ """
+ # A VLAN interface can only be placed in admin up state when
+ # the lower interface is up, too
+ if self.get_vlan_protocol():
+ lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0]
+ with open(lower_interface, 'r') as f:
+ flags = f.read()
+ # If parent is not up - bail out as we can not bring up the VLAN.
+ # Flags are defined in kernel source include/uapi/linux/if.h
+ if not int(flags, 16) & 1:
+ return None
+
+ if state == 'up':
+ self._admin_state_down_cnt -= 1
+ if self._admin_state_down_cnt < 1:
+ return self.set_interface('admin_state', state)
+ else:
+ self._admin_state_down_cnt += 1
+ return self.set_interface('admin_state', state)
+
+ def set_proxy_arp(self, enable):
+ """
+ Set per interface proxy ARP configuration
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_proxy_arp(1)
+ """
+ self.set_interface('proxy_arp', enable)
+
+ def set_proxy_arp_pvlan(self, enable):
+ """
+ Private VLAN proxy arp.
+ Basically allow proxy arp replies back to the same interface
+ (from which the ARP request/solicitation was received).
+
+ This is done to support (ethernet) switch features, like RFC
+ 3069, where the individual ports are NOT allowed to
+ communicate with each other, but they are allowed to talk to
+ the upstream router. As described in RFC 3069, it is possible
+ to allow these hosts to communicate through the upstream
+ router by proxy_arp'ing. Don't need to be used together with
+ proxy_arp.
+
+ This technology is known by different names:
+ In RFC 3069 it is called VLAN Aggregation.
+ Cisco and Allied Telesyn call it Private VLAN.
+ Hewlett-Packard call it Source-Port filtering or port-isolation.
+ Ericsson call it MAC-Forced Forwarding (RFC Draft).
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_proxy_arp_pvlan(1)
+ """
+ self.set_interface('proxy_arp_pvlan', enable)
+
+ def get_addr(self):
+ """
+ Retrieve assigned IPv4 and IPv6 addresses from given interface.
+ This is done using the netifaces and ipaddress python modules.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').get_addrs()
+ ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64']
+ """
+
+ ipv4 = []
+ ipv6 = []
+
+ if AF_INET in ifaddresses(self.config['ifname']).keys():
+ for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]:
+ # we need to manually assemble a list of IPv4 address/prefix
+ prefix = '/' + \
+ str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen)
+ ipv4.append(v4_addr['addr'] + prefix)
+
+ if AF_INET6 in ifaddresses(self.config['ifname']).keys():
+ for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]:
+ # Note that currently expanded netmasks are not supported. That means
+ # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not.
+ # see https://docs.python.org/3/library/ipaddress.html
+ bits = bin(
+ int(v6_addr['netmask'].replace(':', ''), 16)).count('1')
+ prefix = '/' + str(bits)
+
+ # we alsoneed to remove the interface suffix on link local
+ # addresses
+ v6_addr['addr'] = v6_addr['addr'].split('%')[0]
+ ipv6.append(v6_addr['addr'] + prefix)
+
+ return ipv4 + ipv6
+
+ def add_addr(self, addr):
+ """
+ Add IP(v6) address to interface. Address is only added if it is not
+ already assigned to that interface. Address format must be validated
+ and compressed/normalized before calling this function.
+
+ addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
+ IPv4: add IPv4 address to interface
+ IPv6: add IPv6 address to interface
+ dhcp: start dhclient (IPv4) on interface
+ dhcpv6: start WIDE DHCPv6 (IPv6) on interface
+
+ Returns False if address is already assigned and wasn't re-added.
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.add_addr('192.0.2.1/24')
+ >>> j.add_addr('2001:db8::ffff/64')
+ >>> j.get_addr()
+ ['192.0.2.1/24', '2001:db8::ffff/64']
+ """
+ # XXX: normalize/compress with ipaddress if calling functions don't?
+ # is subnet mask always passed, and in the same way?
+
+ # do not add same address twice
+ 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 addr_is_v4 ) ):
+ raise ConfigError((
+ "Can't configure both static IPv4 and DHCP address "
+ "on the same interface"))
+
+ # add to interface
+ if addr == 'dhcp':
+ self.set_dhcp(True)
+ elif addr == 'dhcpv6':
+ self.set_dhcpv6(True)
+ elif not is_intf_addr_assigned(self.ifname, addr):
+ self._cmd(f'ip addr add "{addr}" '
+ f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"')
+ else:
+ return False
+
+ # add to cache
+ self._addr.append(addr)
+
+ return True
+
+ def del_addr(self, addr):
+ """
+ Delete IP(v6) address from interface. Address is only deleted if it is
+ assigned to that interface. Address format must be exactly the same as
+ was used when adding the address.
+
+ addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
+ IPv4: delete IPv4 address from interface
+ IPv6: delete IPv6 address from interface
+ dhcp: stop dhclient (IPv4) on interface
+ dhcpv6: stop dhclient (IPv6) on interface
+
+ Returns False if address isn't already assigned and wasn't deleted.
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> j = Interface('eth0')
+ >>> j.add_addr('2001:db8::ffff/64')
+ >>> j.add_addr('192.0.2.1/24')
+ >>> j.get_addr()
+ ['192.0.2.1/24', '2001:db8::ffff/64']
+ >>> j.del_addr('192.0.2.1/24')
+ >>> j.get_addr()
+ ['2001:db8::ffff/64']
+ """
+
+ # remove from interface
+ if addr == 'dhcp':
+ self.set_dhcp(False)
+ elif addr == 'dhcpv6':
+ self.set_dhcpv6(False)
+ elif is_intf_addr_assigned(self.ifname, addr):
+ self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
+ else:
+ return False
+
+ # remove from cache
+ if addr in self._addr:
+ self._addr.remove(addr)
+
+ return True
+
+ def flush_addrs(self):
+ """
+ Flush all addresses from an interface, including DHCP.
+
+ Will raise an exception on error.
+ """
+ # stop DHCP(v6) if running
+ self.set_dhcp(False)
+ self.set_dhcpv6(False)
+
+ # flush all addresses
+ self._cmd(f'ip addr flush dev "{self.ifname}"')
+
+ def add_to_bridge(self, br):
+ """
+ Adds the interface to the bridge with the passed port config.
+
+ Returns False if bridge doesn't exist.
+ """
+
+ # check if the bridge exists (on boot it doesn't)
+ if br not in Section.interfaces('bridge'):
+ return False
+
+ self.flush_addrs()
+ # add interface to bridge - use Section.klass to get BridgeIf class
+ Section.klass(br)(br, create=False).add_port(self.ifname)
+
+ # TODO: port config (STP)
+
+ return True
+
+ def set_dhcp(self, enable):
+ """
+ Enable/Disable DHCP client on a given interface.
+ """
+ if enable not in [True, False]:
+ raise ValueError()
+
+ ifname = self.ifname
+ config_base = r'/var/lib/dhcp/dhclient'
+ config_file = f'{config_base}_{ifname}.conf'
+ options_file = f'{config_base}_{ifname}.options'
+ pid_file = f'{config_base}_{ifname}.pid'
+ lease_file = f'{config_base}_{ifname}.leases'
+
+ if enable and 'disable' not in self._config:
+ if vyos_dict_search('dhcp_options.host_name', self._config) == None:
+ # read configured system hostname.
+ # maybe change to vyos hostd client ???
+ hostname = 'vyos'
+ with open('/etc/hostname', 'r') as f:
+ hostname = f.read().rstrip('\n')
+ tmp = {'dhcp_options' : { 'host_name' : hostname}}
+ self._config = dict_merge(tmp, self._config)
+
+ render(options_file, 'dhcp-client/daemon-options.tmpl',
+ self._config, trim_blocks=True)
+ render(config_file, 'dhcp-client/ipv4.tmpl',
+ self._config, trim_blocks=True)
+
+ # 'up' check is mandatory b/c even if the interface is A/D, as soon as
+ # the DHCP client is started the interface will be placed in u/u state.
+ # This is not what we intended to do when disabling an interface.
+ return self._cmd(f'systemctl restart dhclient@{ifname}.service')
+ else:
+ self._cmd(f'systemctl stop dhclient@{ifname}.service')
+
+ # cleanup old config files
+ for file in [config_file, options_file, pid_file, lease_file]:
+ if os.path.isfile(file):
+ os.remove(file)
+
+
+ def set_dhcpv6(self, enable):
+ """
+ Enable/Disable DHCPv6 client on a given interface.
+ """
+ if enable not in [True, False]:
+ raise ValueError()
+
+ ifname = self.ifname
+ config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
+
+ if enable and 'disable' not in self._config:
+ render(config_file, 'dhcp-client/ipv6.tmpl',
+ self._config, trim_blocks=True)
+
+ # We must ignore any return codes. This is required to enable DHCPv6-PD
+ # for interfaces which are yet not up and running.
+ return self._popen(f'systemctl restart dhcp6c@{ifname}.service')
+ else:
+ self._popen(f'systemctl stop dhcp6c@{ifname}.service')
+
+ if os.path.isfile(config_file):
+ os.remove(config_file)
+
+
+ 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. """
+
+ # Cache the configuration - it will be reused inside e.g. DHCP handler
+ # XXX: maybe pass the option via __init__ in the future and rename this
+ # method to apply()?
+ self._config = config
+
+ # Update interface description
+ self.set_alias(config.get('description', ''))
+
+ # Ignore link state changes
+ value = '2' if 'disable_link_detect' in config else '1'
+ self.set_link_detect(value)
+
+ # Configure assigned interface IP addresses. No longer
+ # configured addresses will be removed first
+ new_addr = config.get('address', [])
+
+ # XXX: T2636 workaround: convert string to a list with one element
+ if isinstance(new_addr, str):
+ new_addr = [new_addr]
+
+ # always ensure DHCP client is stopped (when not configured explicitly)
+ if 'dhcp' not in new_addr:
+ self.del_addr('dhcp')
+
+ # always ensure DHCPv6 client is stopped (when not configured as client
+ # for IPv6 address or prefix delegation
+ dhcpv6pd = vyos_dict_search('dhcpv6_options.pd', config)
+ if 'dhcpv6' not in new_addr or dhcpv6pd == None:
+ self.del_addr('dhcpv6')
+
+ # 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)
+
+ # start DHCPv6 client when only PD was configured
+ if dhcpv6pd != None:
+ self.set_dhcpv6(True)
+
+ # 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 any(k in ['is_bond_member', 'is_bridge_member'] for k in config):
+ # Bind interface to given VRF or unbind it if vrf node is not set.
+ # unbinding will call 'ip link set dev eth0 nomaster' which will
+ # also drop the interface out of a bridge or bond - thus this is
+ # checked before
+ self.set_vrf(config.get('vrf', ''))
+
+ # Configure ARP cache timeout in milliseconds - has default value
+ tmp = vyos_dict_search('ip.arp_cache_timeout', config)
+ value = tmp if (tmp != None) else '30'
+ self.set_arp_cache_tmo(value)
+
+ # Configure ARP filter configuration
+ tmp = vyos_dict_search('ip.disable_arp_filter', config)
+ value = '0' if (tmp != None) else '1'
+ self.set_arp_filter(value)
+
+ # Configure ARP accept
+ tmp = vyos_dict_search('ip.enable_arp_accept', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_accept(value)
+
+ # Configure ARP announce
+ tmp = vyos_dict_search('ip.enable_arp_announce', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_announce(value)
+
+ # Configure ARP ignore
+ tmp = vyos_dict_search('ip.enable_arp_ignore', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_arp_ignore(value)
+
+ # Enable proxy-arp on this interface
+ tmp = vyos_dict_search('ip.enable_proxy_arp', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_proxy_arp(value)
+
+ # Enable private VLAN proxy ARP on this interface
+ tmp = vyos_dict_search('ip.proxy_arp_pvlan', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_proxy_arp_pvlan(value)
+
+ # IPv6 forwarding
+ tmp = vyos_dict_search('ipv6.disable_forwarding', config)
+ value = '0' if (tmp != None) else '1'
+ self.set_ipv6_forwarding(value)
+
+ # IPv6 router advertisements
+ tmp = vyos_dict_search('ipv6.address.autoconf', config)
+ value = '2' if (tmp != None) else '1'
+ if 'dhcpv6' in new_addr:
+ value = '2'
+ self.set_ipv6_accept_ra(value)
+
+ # IPv6 address autoconfiguration
+ tmp = vyos_dict_search('ipv6.address.autoconf', config)
+ value = '1' if (tmp != None) else '0'
+ self.set_ipv6_autoconf(value)
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ tmp = vyos_dict_search('ipv6.dup_addr_detect_transmits', config)
+ value = tmp if (tmp != None) else '1'
+ self.set_ipv6_dad_messages(value)
+
+ # MTU - Maximum Transfer Unit
+ if 'mtu' in config:
+ self.set_mtu(config.get('mtu'))
+
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ tmp = vyos_dict_search('ipv6.address.eui64_old', config)
+ if tmp:
+ for addr in tmp:
+ self.del_ipv6_eui64_address(addr)
+
+ # Change interface MAC address - re-set to real hardware address (hw-id)
+ # if custom mac is removed. Skip if bond member.
+ if 'is_bond_member' not in config:
+ mac = config.get('hw_id')
+ if 'mac' in config:
+ mac = config.get('mac')
+ if mac:
+ self.set_mac(mac)
+
+ # Manage IPv6 link-local addresses
+ tmp = vyos_dict_search('ipv6.address.no_default_link_local', config)
+ # we must check explicitly for None type as if the key is set we will
+ # get an empty dict (<class 'dict'>)
+ if tmp is not None:
+ self.del_ipv6_eui64_address('fe80::/64')
+ else:
+ self.add_ipv6_eui64_address('fe80::/64')
+
+ # Add IPv6 EUI-based addresses
+ tmp = vyos_dict_search('ipv6.address.eui64', config)
+ if tmp:
+ # XXX: T2636 workaround: convert string to a list with one element
+ if isinstance(tmp, str):
+ tmp = [tmp]
+ for addr in tmp:
+ self.add_ipv6_eui64_address(addr)
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if 'is_bridge_member' in config:
+ bridge = config.get('is_bridge_member')
+ self.add_to_bridge(bridge)
+
+ # remove no longer required 802.1ad (Q-in-Q VLANs)
+ for vif_s_id in config.get('vif_s_remove', {}):
+ self.del_vlan(vif_s_id)
+
+ # create/update 802.1ad (Q-in-Q VLANs)
+ ifname = config['ifname']
+ for vif_s_id, vif_s in config.get('vif_s', {}).items():
+ tmp=get_ethertype(vif_s.get('ethertype', '0x88A8'))
+ s_vlan = self.add_vlan(vif_s_id, ethertype=tmp)
+ vif_s['ifname'] = f'{ifname}.{vif_s_id}'
+ s_vlan.update(vif_s)
+
+ # remove no longer required client VLAN (vif-c)
+ for vif_c_id in vif_s.get('vif_c_remove', {}):
+ s_vlan.del_vlan(vif_c_id)
+
+ # create/update client VLAN (vif-c) interface
+ for vif_c_id, vif_c in vif_s.get('vif_c', {}).items():
+ c_vlan = s_vlan.add_vlan(vif_c_id)
+ vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}'
+ c_vlan.update(vif_c)
+
+ # remove no longer required 802.1q VLAN interfaces
+ for vif_id in config.get('vif_remove', {}):
+ self.del_vlan(vif_id)
+
+ # create/update 802.1q VLAN interfaces
+ for vif_id, vif in config.get('vif', {}).items():
+ vlan = self.add_vlan(vif_id)
+ vif['ifname'] = f'{ifname}.{vif_id}'
+ vlan.update(vif)
diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py
new file mode 100644
index 000000000..34147eb38
--- /dev/null
+++ b/python/vyos/ifconfig/l2tpv3.py
@@ -0,0 +1,113 @@
+# Copyright 2019 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
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class L2TPv3If(Interface):
+ """
+ The Linux bonding driver provides a method for aggregating multiple network
+ interfaces into a single logical "bonded" interface. The behavior of the
+ bonded interfaces depends upon the mode; generally speaking, modes provide
+ either hot standby or load balancing services. Additionally, link integrity
+ monitoring may be performed.
+ """
+
+ default = {
+ 'type': 'l2tp',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'l2tpeth',
+ 'prefixes': ['l2tpeth', ],
+ 'bridgeable': True,
+ }
+ }
+ options = Interface.options + \
+ ['tunnel_id', 'peer_tunnel_id', 'local_port', 'remote_port',
+ 'encapsulation', 'local_address', 'remote_address', 'session_id',
+ 'peer_session_id']
+
+ def _create(self):
+ # create tunnel interface
+ cmd = 'ip l2tp add tunnel tunnel_id {tunnel_id}'
+ cmd += ' peer_tunnel_id {peer_tunnel_id}'
+ cmd += ' udp_sport {local_port}'
+ cmd += ' udp_dport {remote_port}'
+ cmd += ' encap {encapsulation}'
+ cmd += ' local {local_address}'
+ cmd += ' remote {remote_address}'
+ self._cmd(cmd.format(**self.config))
+
+ # setup session
+ cmd = 'ip l2tp add session name {ifname}'
+ cmd += ' tunnel_id {tunnel_id}'
+ cmd += ' session_id {session_id}'
+ cmd += ' peer_session_id {peer_session_id}'
+ self._cmd(cmd.format(**self.config))
+
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses.
+ Example:
+ >>> from vyos.ifconfig import L2TPv3If
+ >>> i = L2TPv3If('l2tpeth0')
+ >>> i.remove()
+ """
+
+ if os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])):
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
+ if self.config['tunnel_id'] and self.config['session_id']:
+ cmd = 'ip l2tp del session tunnel_id {tunnel_id}'
+ cmd += ' session_id {session_id}'
+ self._cmd(cmd.format(**self.config))
+
+ if self.config['tunnel_id']:
+ cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}'
+ self._cmd(cmd.format(**self.config))
+
+ @staticmethod
+ def get_config():
+ """
+ L2TPv3 interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = L2TPv3If().get_config()
+ """
+ config = {
+ 'peer_tunnel_id': '',
+ 'local_port': 0,
+ 'remote_port': 0,
+ 'encapsulation': 'udp',
+ 'local_address': '',
+ 'remote_address': '',
+ 'session_id': '',
+ 'tunnel_id': '',
+ 'peer_session_id': ''
+ }
+ return config
diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py
new file mode 100644
index 000000000..2b4ebfdcc
--- /dev/null
+++ b/python/vyos/ifconfig/loopback.py
@@ -0,0 +1,89 @@
+# Copyright 2019 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 vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+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',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'loopback',
+ 'prefixes': ['lo', ],
+ 'bridgeable': True,
+ }
+ }
+
+ name = 'loopback'
+
+ def remove(self):
+ """
+ Loopback interface can not be deleted from operating system. We can
+ only remove all assigned IP addresses.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = LoopbackIf('lo').remove()
+ """
+ # remove all assigned IP addresses from interface
+ for addr in self.get_addr():
+ 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})
+
+ # call base class
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py
new file mode 100644
index 000000000..6f570d162
--- /dev/null
+++ b/python/vyos/ifconfig/macsec.py
@@ -0,0 +1,92 @@
+# 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 vyos.ifconfig.interface import Interface
+
+@Interface.register
+class MACsecIf(Interface):
+ """
+ MACsec is an IEEE standard (IEEE 802.1AE) for MAC security, introduced in
+ 2006. It defines a way to establish a protocol independent connection
+ between two hosts with data confidentiality, authenticity and/or integrity,
+ using GCM-AES-128. MACsec operates on the Ethernet layer and as such is a
+ layer 2 protocol, which means it's designed to secure traffic within a
+ layer 2 network, including DHCP or ARP requests. It does not compete with
+ other security solutions such as IPsec (layer 3) or TLS (layer 4), as all
+ those solutions are used for their own specific use cases.
+ """
+
+ default = {
+ 'type': 'macsec',
+ 'security_cipher': '',
+ 'source_interface': ''
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'macsec',
+ 'prefixes': ['macsec', ],
+ },
+ }
+ options = Interface.options + \
+ ['security_cipher', 'source_interface']
+
+ def _create(self):
+ """
+ Create MACsec interface in OS kernel. Interface is administrative
+ down by default.
+ """
+ # create tunnel interface
+ cmd = 'ip link add link {source_interface} {ifname} type {type}'
+ cmd += ' cipher {security_cipher}'
+ self._cmd(cmd.format(**self.config))
+
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
+ @staticmethod
+ def get_config():
+ """
+ MACsec interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = MACsecIf().get_config()
+ """
+ config = {
+ 'security_cipher': '',
+ 'source_interface': '',
+ }
+ return config
+
+ 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. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py
new file mode 100644
index 000000000..b068ce873
--- /dev/null
+++ b/python/vyos/ifconfig/macvlan.py
@@ -0,0 +1,89 @@
+# Copyright 2019 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 copy import deepcopy
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.vlan import VLAN
+
+
+@Interface.register
+@VLAN.enable
+class MACVLANIf(Interface):
+ """
+ Abstraction of a Linux MACvlan interface
+ """
+
+ default = {
+ 'type': 'macvlan',
+ 'address': '',
+ 'source_interface': '',
+ 'mode': '',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'pseudo-ethernet',
+ 'prefixes': ['peth', ],
+ },
+ }
+ options = Interface.options + \
+ ['source_interface', 'mode']
+
+ def _create(self):
+ # please do not change the order when assembling the command
+ cmd = 'ip link add {ifname}'
+ if self.config['source_interface']:
+ cmd += ' link {source_interface}'
+ cmd += ' type macvlan'
+ if self.config['mode']:
+ cmd += ' mode {mode}'
+ self._cmd(cmd.format(**self.config))
+
+ def set_mode(self, mode):
+ ifname = self.config['ifname']
+ cmd = f'ip link set dev {ifname} type macvlan mode {mode}'
+ return self._cmd(cmd)
+
+ @classmethod
+ def get_config(cls):
+ """
+ MACVLAN interfaces require a configuration when they are added using
+ iproute2. This method will provide the configuration dictionary used
+ by this class.
+
+ Example:
+ >> dict = MACVLANIf().get_config()
+ """
+ return deepcopy(cls.default)
+
+ 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. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py
new file mode 100644
index 000000000..d585c1873
--- /dev/null
+++ b/python/vyos/ifconfig/operational.py
@@ -0,0 +1,179 @@
+# Copyright 2019 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
+from time import time
+from datetime import datetime
+from functools import reduce
+
+from tabulate import tabulate
+
+from vyos.ifconfig import Control
+
+
+class Operational(Control):
+ """
+ A class able to load Interface statistics
+ """
+
+ cache_magic = 'XYZZYX'
+
+ _stat_names = {
+ 'rx': ['bytes', 'packets', 'errors', 'dropped', 'overrun', 'mcast'],
+ 'tx': ['bytes', 'packets', 'errors', 'dropped', 'carrier', 'collisions'],
+ }
+
+ _stats_dir = {
+ 'rx': ['rx_bytes', 'rx_packets', 'rx_errors', 'rx_dropped', 'rx_over_errors', 'multicast'],
+ 'tx': ['tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped', 'tx_carrier_errors', 'collisions'],
+ }
+
+ # a list made of the content of _stats_dir['rx'] + _stats_dir['tx']
+ _stats_all = reduce(lambda x, y: x+y, _stats_dir.values())
+
+ # this is not an interface but will be able to be controlled like one
+ _sysfs_get = {
+ 'oper_state':{
+ 'location': '/sys/class/net/{ifname}/operstate',
+ },
+ }
+
+
+ @classmethod
+ def cachefile (cls, ifname):
+ # the file where we are saving the counters
+ return f'/var/run/vyatta/{ifname}.stats'
+
+
+ def __init__(self, ifname):
+ """
+ Operational provide access to the counters of an interface
+ It behave like an interface when it comes to access sysfs
+
+ interface is an instance of the interface for which we want
+ to look at (a subclass of Interface, such as EthernetIf)
+ """
+
+ # add a self.config to minic Interface behaviour and make
+ # coding similar. Perhaps part of class Interface could be
+ # moved into a shared base class.
+ self.config = {
+ 'ifname': ifname,
+ 'create': False,
+ 'debug': False,
+ }
+ super().__init__(**self.config)
+ self.ifname = ifname
+
+ # adds all the counters of an interface
+ for stat in self._stats_all:
+ self._sysfs_get[stat] = {
+ 'location': '/sys/class/net/{ifname}/statistics/'+stat,
+ }
+
+ def get_state(self):
+ """
+ Get interface operational state
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').operational.get_sate()
+ 'up'
+ """
+ # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net
+ # "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up"
+ return self.get_interface('oper_state')
+
+ @classmethod
+ def strtime (cls, epoc):
+ """
+ represent an epoc/unix date in the format used by operation commands
+ """
+ return datetime.fromtimestamp(epoc).strftime("%a %b %d %R:%S %Z %Y")
+
+ def save_counters(self, stats):
+ """
+ record the provided stats to a file keeping vyatta compatibility
+ """
+
+ with open(self.cachefile(self.ifname), 'w') as f:
+ f.write(self.cache_magic)
+ f.write('\n')
+ f.write(str(int(time())))
+ f.write('\n')
+ for k,v in stats.items():
+ if v:
+ f.write(f'{k},{v}\n')
+
+ def load_counters(self):
+ """
+ load the stats from a file keeping vyatta compatibility
+ return a dict() with the value for each interface counter for the cache
+ """
+ ifname = self.config['ifname']
+
+ stats = {}
+ no_stats = {}
+ for name in self._stats_all:
+ stats[name] = 0
+ no_stats[name] = 0
+
+ try:
+ with open(self.cachefile(self.ifname),'r') as f:
+ magic = f.readline().strip()
+ if magic != self.cache_magic:
+ print(f'bad magic {ifname}')
+ return no_stats
+ stats['timestamp'] = f.readline().strip()
+ for line in f:
+ k, v = line.split(',')
+ stats[k] = int(v)
+ return stats
+ except IOError:
+ return no_stats
+
+ def clear_counters(self, counters=None):
+ clear = self._stats_all if counters is None else []
+ stats = self.load_counters()
+ for counter, value in stats.items():
+ stats[counter] = 0 if counter in clear else value
+ self.save_counters(stats)
+
+ def reset_counters(self):
+ os.remove(self.cachefile(self.ifname))
+
+ def get_stats(self):
+ """ return a dict() with the value for each interface counter """
+ stats = {}
+ for counter in self._stats_all:
+ stats[counter] = int(self.get_interface(counter))
+ return stats
+
+ def formated_stats(self, indent=4):
+ tabs = []
+ stats = self.get_stats()
+ for rtx in self._stats_dir:
+ tabs.append([f'{rtx.upper()}:', ] + [_ for _ in self._stat_names[rtx]])
+ tabs.append(['', ] + [stats[_] for _ in self._stats_dir[rtx]])
+
+ s = tabulate(
+ tabs,
+ stralign="right",
+ numalign="right",
+ tablefmt="plain"
+ )
+
+ p = ' '*indent
+ return f'{p}' + s.replace('\n', f'\n{p}')
diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py
new file mode 100644
index 000000000..787245696
--- /dev/null
+++ b/python/vyos/ifconfig/pppoe.py
@@ -0,0 +1,41 @@
+# 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 vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class PPPoEIf(Interface):
+ default = {
+ 'type': 'pppoe',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'pppoe',
+ 'prefixes': ['pppoe', ],
+ },
+ }
+
+ # stub this interface is created in the configure script
+
+ def _create(self):
+ # we can not create this interface as it is managed outside
+ pass
+
+ def _delete(self):
+ # we can not create this interface as it is managed outside
+ pass
diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py
new file mode 100644
index 000000000..173a90bb4
--- /dev/null
+++ b/python/vyos/ifconfig/section.py
@@ -0,0 +1,189 @@
+# 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 re
+import netifaces
+
+
+class Section:
+ # the known interface prefixes
+ _prefixes = {}
+ _classes = []
+
+ # class need to define: definition['prefixes']
+ # the interface prefixes declared by a class used to name interface with
+ # prefix[0-9]*(\.[0-9]+)?(\.[0-9]+)?, such as lo, eth0 or eth0.1.2
+
+ @classmethod
+ def register(cls, klass):
+ """
+ A function to use as decorator the interfaces classes
+ It register the prefix for the interface (eth, dum, vxlan, ...)
+ with the class which can handle it (EthernetIf, DummyIf,VXLANIf, ...)
+ """
+ if not klass.definition.get('prefixes',[]):
+ raise RuntimeError(f'valid interface prefixes not defined for {klass.__name__}')
+
+ cls._classes.append(klass)
+
+ for ifprefix in klass.definition['prefixes']:
+ if ifprefix in cls._prefixes:
+ raise RuntimeError(f'only one class can be registered for prefix "{ifprefix}" type')
+ cls._prefixes[ifprefix] = klass
+
+ return klass
+
+ @classmethod
+ def _basename (cls, name, vlan):
+ """
+ remove the number at the end of interface name
+ name: name of the interface
+ vlan: if vlan is True, do not stop at the vlan number
+ """
+ name = name.rstrip('0123456789')
+ name = name.rstrip('.')
+ if vlan:
+ name = name.rstrip('0123456789.')
+ return name
+
+ @classmethod
+ def section(cls, name, vlan=True):
+ """
+ return the name of a section an interface should be under
+ name: name of the interface (eth0, dum1, ...)
+ vlan: should we try try to remove the VLAN from the number
+ """
+ name = cls._basename(name, vlan)
+
+ if name in cls._prefixes:
+ return cls._prefixes[name].definition['section']
+ return ''
+
+ @classmethod
+ def sections(cls):
+ """
+ return all the sections we found under 'set interfaces'
+ """
+ return list(set([cls._prefixes[_].definition['section'] for _ in cls._prefixes]))
+
+ @classmethod
+ def klass(cls, name, vlan=True):
+ name = cls._basename(name, vlan)
+ if name in cls._prefixes:
+ return cls._prefixes[name]
+ raise ValueError(f'No type found for interface name: {name}')
+
+ @classmethod
+ def _intf_under_section (cls,section=''):
+ """
+ return a generator with the name of the configured interface
+ which are under a section
+ """
+ interfaces = netifaces.interfaces()
+
+ for ifname in interfaces:
+ ifsection = cls.section(ifname)
+ if not ifsection:
+ continue
+
+ if section and ifsection != section:
+ continue
+
+ yield ifname
+
+ @classmethod
+ def _sort_interfaces(cls, generator):
+ """
+ return a list of the sorted interface by number, vlan, qinq
+ """
+ def key(ifname):
+ value = 0
+ parts = re.split(r'([^0-9]+)([0-9]+)[.]?([0-9]+)?[.]?([0-9]+)?', ifname)
+ length = len(parts)
+ name = parts[1] if length >= 3 else parts[0]
+ # the +1 makes sure eth0.0.0 after eth0.0
+ number = int(parts[2]) + 1 if length >= 4 and parts[2] is not None else 0
+ vlan = int(parts[3]) + 1 if length >= 5 and parts[3] is not None else 0
+ qinq = int(parts[4]) + 1 if length >= 6 and parts[4] is not None else 0
+
+ # so that "lo" (or short names) are handled (as "loa")
+ for n in (name + 'aaa')[:3]:
+ value *= 100
+ value += (ord(n) - ord('a'))
+ value += number
+ # vlan are 16 bits, so this can not overflow
+ value = (value << 16) + vlan
+ value = (value << 16) + qinq
+ return value
+
+ l = list(generator)
+ l.sort(key=key)
+ return l
+
+ @classmethod
+ def interfaces(cls, section=''):
+ """
+ return a list of the name of the configured interface which are under a section
+ if no section is provided, then it returns all configured interfaces
+ """
+
+ return cls._sort_interfaces(cls._intf_under_section(section))
+
+ @classmethod
+ def _intf_with_feature(cls, feature=''):
+ """
+ return a generator with the name of the configured interface which have
+ a particular feature set in their definition such as:
+ bondable, broadcast, bridgeable, ...
+ """
+ for klass in cls._classes:
+ if klass.definition[feature]:
+ yield klass.definition['section']
+
+ @classmethod
+ def feature(cls, feature=''):
+ """
+ return list with the name of the configured interface which have
+ a particular feature set in their definition such as:
+ bondable, broadcast, bridgeable, ...
+ """
+ return list(cls._intf_with_feature(feature))
+
+ @classmethod
+ def reserved(cls):
+ """
+ return list with the interface name prefixes
+ eth, lo, vxlan, dum, ...
+ """
+ return list(cls._prefixes.keys())
+
+ @classmethod
+ def get_config_path(cls, name):
+ """
+ get config path to interface with .vif or .vif-s.vif-c
+ example: eth0.1.2 -> 'ethernet eth0 vif-s 1 vif-c 2'
+ Returns False if interface name is invalid (not found in sections)
+ """
+ sect = cls.section(name)
+ if sect:
+ splinterface = name.split('.')
+ intfpath = f'{sect} {splinterface[0]}'
+ if len(splinterface) == 2:
+ intfpath += f' vif {splinterface[1]}'
+ elif len(splinterface) == 3:
+ intfpath += f' vif-s {splinterface[1]} vif-c {splinterface[2]}'
+ return intfpath
+ else:
+ return False
diff --git a/python/vyos/ifconfig/stp.py b/python/vyos/ifconfig/stp.py
new file mode 100644
index 000000000..5e83206c2
--- /dev/null
+++ b/python/vyos/ifconfig/stp.py
@@ -0,0 +1,70 @@
+# Copyright 2019 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 vyos.ifconfig.interface import Interface
+
+from vyos.validate import assert_positive
+
+
+class STP:
+ """
+ A spanning-tree capable interface. This applies only to bridge port member
+ interfaces!
+ """
+
+ @classmethod
+ def enable (cls, adaptee):
+ adaptee._sysfs_set = {**adaptee._sysfs_set, **cls._sysfs_set}
+ adaptee.set_path_cost = cls.set_path_cost
+ adaptee.set_path_priority = cls.set_path_priority
+ return adaptee
+
+ _sysfs_set = {
+ 'path_cost': {
+ # XXX: we should set a maximum
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/brport/path_cost',
+ 'errormsg': '{ifname} is not a bridge port member'
+ },
+ 'path_priority': {
+ # XXX: we should set a maximum
+ 'validate': assert_positive,
+ 'location': '/sys/class/net/{ifname}/brport/priority',
+ 'errormsg': '{ifname} is not a bridge port member'
+ },
+ }
+
+ def set_path_cost(self, cost):
+ """
+ Set interface path cost, only relevant for STP enabled interfaces
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_path_cost(4)
+ """
+ self.set_interface('path_cost', cost)
+
+ def set_path_priority(self, priority):
+ """
+ Set interface path priority, only relevant for STP enabled interfaces
+
+ Example:
+
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_path_priority(4)
+ """
+ self.set_interface('path_priority', priority)
diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py
new file mode 100644
index 000000000..85c22b5b4
--- /dev/null
+++ b/python/vyos/ifconfig/tunnel.py
@@ -0,0 +1,338 @@
+# Copyright 2019 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/>.
+
+# https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/
+# https://community.hetzner.com/tutorials/linux-setup-gre-tunnel
+
+
+from copy import deepcopy
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.afi import IP4, IP6
+from vyos.validate import assert_list
+
+def enable_to_on(value):
+ if value == 'enable':
+ return 'on'
+ if value == 'disable':
+ return 'off'
+ raise ValueError(f'expect enable or disable but got "{value}"')
+
+
+@Interface.register
+class _Tunnel(Interface):
+ """
+ _Tunnel: private base class for tunnels
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/tunnel.c
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip6tunnel.c
+ """
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'tunnel',
+ 'prefixes': ['tun',],
+ 'bridgeable': False,
+ },
+ }
+
+ # TODO: This is surely used for more than tunnels
+ # TODO: could be refactored elsewhere
+ _command_set = {**Interface._command_set, **{
+ 'multicast': {
+ 'validate': lambda v: assert_list(v, ['enable', 'disable']),
+ 'convert': enable_to_on,
+ 'shellcmd': 'ip link set dev {ifname} multicast {value}',
+ },
+ 'allmulticast': {
+ 'validate': lambda v: assert_list(v, ['enable', 'disable']),
+ 'convert': enable_to_on,
+ 'shellcmd': 'ip link set dev {ifname} allmulticast {value}',
+ },
+ }}
+
+ # use for "options" and "updates"
+ # If an key is only in the options list, it can only be set at creation time
+ # the create comand will only be make using the key in options
+
+ # If an option is in the updates list, it can be updated
+ # upon, the creation, all key not yet applied will be updated
+
+ # multicast/allmulticast can not be part of the create command
+
+ # options matrix:
+ # with ip = 4, we have multicast
+ # wiht ip = 6, nothing
+ # with tunnel = 4, we have tos, ttl, key
+ # with tunnel = 6, we have encaplimit, hoplimit, tclass, flowlabel
+
+ # TODO: For multicast, it is allowed on IP6IP6 and Sit6RD
+ # TODO: to match vyatta but it should be checked for correctness
+
+ updates = []
+
+ create = ''
+ change = ''
+ delete = ''
+
+ ip = [] # AFI of the families which can be used in the tunnel
+ tunnel = 0 # invalid - need to be set by subclasses
+
+ def __init__(self, ifname, **config):
+ self.config = deepcopy(config) if config else {}
+ super().__init__(ifname, **config)
+
+ def _create(self):
+ # add " option-name option-name-value ..." for all options set
+ options = " ".join(["{} {}".format(k, self.config[k])
+ for k in self.options if k in self.config and self.config[k]])
+ self._cmd('{} {}'.format(self.create.format(**self.config), options))
+ self.set_admin_state('down')
+
+ def _delete(self):
+ self.set_admin_state('down')
+ cmd = self.delete.format(**self.config)
+ return self._cmd(cmd)
+
+ def set_interface(self, option, value):
+ try:
+ return Interface.set_interface(self, option, value)
+ except Exception:
+ pass
+
+ if value == '':
+ # remove the value so that it is not used
+ self.config.pop(option, '')
+
+ if self.change:
+ self._cmd('{} {} {}'.format(
+ self.change.format(**self.config), option, value))
+ return True
+
+ @classmethod
+ def get_config(cls):
+ return dict(zip(cls.options, ['']*len(cls.options)))
+
+
+class GREIf(_Tunnel):
+ """
+ GRE: Generic Routing Encapsulation
+
+ For more information please refer to:
+ RFC1701, RFC1702, RFC2784
+ https://tools.ietf.org/html/rfc2784
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre.c
+ """
+
+ definition = {
+ **_Tunnel.definition,
+ **{
+ 'bridgeable': True,
+ },
+ }
+
+ ip = [IP4, IP6]
+ tunnel = IP4
+
+ default = {'type': 'gre'}
+ required = ['local', ] # mGRE is a GRE without remote endpoint
+
+ options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'dev', 'ttl', 'tos',
+ 'mtu', 'multicast', 'allmulticast']
+
+ create = 'ip tunnel add {ifname} mode {type}'
+ change = 'ip tunnel cha {ifname}'
+ delete = 'ip tunnel del {ifname}'
+
+
+# GreTap also called GRE Bridge
+class GRETapIf(_Tunnel):
+ """
+ GRETapIF: GreIF using TAP instead of TUN
+
+ https://en.wikipedia.org/wiki/TUN/TAP
+ """
+
+ # no multicast, ttl or tos for gretap
+
+ definition = {
+ **_Tunnel.definition,
+ **{
+ 'bridgeable': True,
+ },
+ }
+
+ ip = [IP4, ]
+ tunnel = IP4
+
+ default = {'type': 'gretap'}
+ required = ['local', ]
+
+ options = ['local', 'remote', ]
+ updates = ['mtu', ]
+
+ create = 'ip link add {ifname} type {type}'
+ change = ''
+ delete = 'ip link del {ifname}'
+
+
+class IP6GREIf(_Tunnel):
+ """
+ IP6Gre: IPv6 Support for Generic Routing Encapsulation (GRE)
+
+ For more information please refer to:
+ https://tools.ietf.org/html/rfc7676
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre6.c
+ """
+
+ ip = [IP4, IP6]
+ tunnel = IP6
+
+ default = {'type': 'ip6gre'}
+ required = ['local', 'remote']
+
+ options = ['local', 'remote', 'dev', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel']
+ updates = ['local', 'remote', 'dev', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel',
+ 'mtu', 'multicast', 'allmulticast']
+
+ create = 'ip tunnel add {ifname} mode {type}'
+ change = 'ip tunnel cha {ifname} mode {type}'
+ delete = 'ip tunnel del {ifname}'
+
+ # using "ip tunnel change" without using "mode" causes errors
+ # sudo ip tunnel add tun100 mode ip6gre local ::1 remote 1::1
+ # sudo ip tunnel cha tun100 hoplimit 100
+ # *** stack smashing detected ** *: < unknown > terminated
+ # sudo ip tunnel cha tun100 local: : 2
+ # Error: an IP address is expected rather than "::2"
+ # works if mode is explicit
+
+
+class IPIPIf(_Tunnel):
+ """
+ IPIP: IP Encapsulation within IP
+
+ For more information please refer to:
+ https://tools.ietf.org/html/rfc2003
+ """
+
+ # IPIP does not allow to pass multicast, unlike GRE
+ # but the interface itself can be set with multicast
+
+ ip = [IP4,]
+ tunnel = IP4
+
+ default = {'type': 'ipip'}
+ required = ['local', 'remote']
+
+ options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'dev', 'ttl', 'tos',
+ 'mtu', 'multicast', 'allmulticast']
+
+ create = 'ip tunnel add {ifname} mode {type}'
+ change = 'ip tunnel cha {ifname}'
+ delete = 'ip tunnel del {ifname}'
+
+
+class IPIP6If(_Tunnel):
+ """
+ IPIP6: IPv4 over IPv6 tunnel
+
+ For more information please refer to:
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_ip6tnl.c
+ """
+
+ ip = [IP4,]
+ tunnel = IP6
+
+ default = {'type': 'ipip6'}
+ required = ['local', 'remote']
+
+ options = ['local', 'remote', 'dev', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel']
+ updates = ['local', 'remote', 'dev', 'encaplimit',
+ 'hoplimit', 'tclass', 'flowlabel',
+ 'mtu', 'multicast', 'allmulticast']
+
+ create = 'ip -6 tunnel add {ifname} mode {type}'
+ change = 'ip -6 tunnel cha {ifname}'
+ delete = 'ip -6 tunnel del {ifname}'
+
+
+class IP6IP6If(IPIP6If):
+ """
+ IP6IP6: IPv6 over IPv6 tunnel
+
+ For more information please refer to:
+ https://tools.ietf.org/html/rfc2473
+ """
+
+ ip = [IP6,]
+
+ default = {'type': 'ip6ip6'}
+
+
+class SitIf(_Tunnel):
+ """
+ Sit: Simple Internet Transition
+
+ For more information please refer to:
+ https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_iptnl.c
+ """
+
+ ip = [IP6, IP4]
+ tunnel = IP4
+
+ default = {'type': 'sit'}
+ required = ['local', 'remote']
+
+ options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key']
+ updates = ['local', 'remote', 'dev', 'ttl', 'tos',
+ 'mtu', 'multicast', 'allmulticast']
+
+ create = 'ip tunnel add {ifname} mode {type}'
+ change = 'ip tunnel cha {ifname}'
+ delete = 'ip tunnel del {ifname}'
+
+
+class Sit6RDIf(SitIf):
+ """
+ Sit6RDIf: Simple Internet Transition with 6RD
+
+ https://en.wikipedia.org/wiki/IPv6_rapid_deployment
+ """
+
+ ip = [IP6,]
+
+ required = ['remote', '6rd-prefix']
+
+ # TODO: check if key can really be used with 6RD
+ options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix']
+ updates = ['remote', 'ttl', 'tos',
+ 'mtu', 'multicast', 'allmulticast']
+
+ def _create(self):
+ # do not call _Tunnel.create, building fully here
+
+ create = 'ip tunnel add {ifname} mode {type} remote {remote}'
+ self._cmd(create.format(**self.config))
+ self.set_interface('state','down')
+
+ set6rd = 'ip tunnel 6rd dev {ifname} 6rd-prefix {6rd-prefix}'
+ if '6rd-relay-prefix' in self.config:
+ set6rd += ' 6rd-relay-prefix {6rd-relay-prefix}'
+ self._cmd(set6rd.format(**self.config))
diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py
new file mode 100644
index 000000000..d68e8f6cd
--- /dev/null
+++ b/python/vyos/ifconfig/vlan.py
@@ -0,0 +1,142 @@
+# Copyright 2019 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
+
+from vyos.ifconfig.interface import Interface
+
+
+# This is an internal implementation class
+class VLAN:
+ """
+ This class handels the creation and removal of a VLAN interface. It serves
+ as base class for BondIf and EthernetIf.
+ """
+
+ _novlan_remove = lambda : None
+
+ @classmethod
+ def enable (cls,adaptee):
+ adaptee._novlan_remove = adaptee.remove
+ adaptee.remove = cls.remove
+ adaptee.add_vlan = cls.add_vlan
+ adaptee.del_vlan = cls.del_vlan
+ adaptee.definition['vlan'] = True
+ return adaptee
+
+ def remove(self):
+ """
+ Remove interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> i = Interface('eth0')
+ >>> i.remove()
+ """
+ ifname = self.config['ifname']
+
+ # Do we have sub interfaces (VLANs)? We apply a regex matching
+ # subinterfaces (indicated by a .) of a parent interface.
+ #
+ # As interfaces need to be deleted "in order" starting from Q-in-Q
+ # we delete them first.
+ vlan_ifs = [f for f in os.listdir(r'/sys/class/net')
+ if re.match(ifname + r'(?:\.\d+)(?:\.\d+)', f)]
+
+ for vlan in vlan_ifs:
+ Interface(vlan).remove()
+
+ # After deleting all Q-in-Q interfaces delete other VLAN interfaces
+ # which probably acted as parent to Q-in-Q or have been regular 802.1q
+ # interface.
+ vlan_ifs = [f for f in os.listdir(r'/sys/class/net')
+ if re.match(ifname + r'(?:\.\d+)', f)]
+
+ for vlan in vlan_ifs:
+ # self.__class__ is already VLAN.enabled
+ self.__class__(vlan)._novlan_remove()
+
+ # All subinterfaces are now removed, continue on the physical interface
+ self._novlan_remove()
+
+ def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''):
+ """
+ A virtual LAN (VLAN) is any broadcast domain that is partitioned and
+ isolated in a computer network at the data link layer (OSI layer 2).
+ Use this function to create a new VLAN interface on a given physical
+ interface.
+
+ This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto
+ parameter is used to indicate VLAN type.
+
+ A new object of type VLANIf is returned once the interface has been
+ created.
+
+ @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN
+ interface
+ @param ingress_qos: Defines a mapping of VLAN header prio field to the
+ Linux internal packet priority on incoming frames.
+ @param ingress_qos: Defines a mapping of Linux internal packet priority
+ to VLAN header prio field but for outgoing frames.
+
+ Example:
+ >>> from vyos.ifconfig import MACVLANIf
+ >>> i = MACVLANIf('eth0')
+ >>> i.add_vlan(10)
+ """
+ vlan_ifname = self.config['ifname'] + '.' + str(vlan_id)
+ if os.path.exists(f'/sys/class/net/{vlan_ifname}'):
+ return self.__class__(vlan_ifname)
+
+ if ethertype:
+ self._ethertype = ethertype
+ ethertype = 'proto {}'.format(ethertype)
+
+ # Optional ingress QOS mapping
+ opt_i = ''
+ if ingress_qos:
+ opt_i = 'ingress-qos-map ' + ingress_qos
+ # Optional egress QOS mapping
+ opt_e = ''
+ if egress_qos:
+ opt_e = 'egress-qos-map ' + egress_qos
+
+ # create interface in the system
+ cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
+ .format(ifname=self.ifname, vlan=vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
+ self._cmd(cmd)
+
+ # return new object mapping to the newly created interface
+ # we can now work on this object for e.g. IP address setting
+ # or interface description and so on
+ return self.__class__(vlan_ifname)
+
+ def del_vlan(self, vlan_id):
+ """
+ Remove VLAN interface from operating system. Removing the interface
+ deconfigures all assigned IP addresses and clear possible DHCP(v6)
+ client processes.
+
+ Example:
+ >>> from vyos.ifconfig import MACVLANIf
+ >>> i = MACVLANIf('eth0.10')
+ >>> i.del_vlan()
+ """
+ ifname = self.config['ifname']
+ self.__class__(f'{ifname}.{vlan_id}')._novlan_remove()
diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py
new file mode 100644
index 000000000..01a7cc7ab
--- /dev/null
+++ b/python/vyos/ifconfig/vrrp.py
@@ -0,0 +1,151 @@
+# Copyright 2019 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 json
+import signal
+from time import time
+from time import sleep
+
+from tabulate import tabulate
+
+from vyos import airbag
+from vyos import util
+
+
+class VRRPError(Exception):
+ pass
+
+class VRRPNoData(VRRPError):
+ pass
+
+class VRRP(object):
+ _vrrp_prefix = '00:00:5E:00:01:'
+ location = {
+ 'pid': '/run/keepalived.pid',
+ 'fifo': '/run/keepalived_notify_fifo',
+ 'state': '/tmp/keepalived.data',
+ 'stats': '/tmp/keepalived.stats',
+ 'json': '/tmp/keepalived.json',
+ 'daemon': '/etc/default/keepalived',
+ 'config': '/etc/keepalived/keepalived.conf',
+ 'vyos': '/run/keepalived_config.dict',
+ }
+
+ _signal = {
+ 'state': signal.SIGUSR1,
+ 'stats': signal.SIGUSR2,
+ 'json': signal.SIGRTMIN + 2,
+ }
+
+ _name = {
+ 'state': 'information',
+ 'stats': 'statistics',
+ 'json': 'data',
+ }
+
+ state = {
+ 0: 'INIT',
+ 1: 'BACKUP',
+ 2: 'MASTER',
+ 3: 'FAULT',
+ # UNKNOWN
+ }
+
+ def __init__(self,ifname):
+ self.ifname = ifname
+
+ def enabled(self):
+ return self.ifname in self.active_interfaces()
+
+ @classmethod
+ def active_interfaces(cls):
+ if not os.path.exists(cls.location['pid']):
+ return []
+ data = cls.collect('json')
+ return [group['data']['ifp_ifname'] for group in json.loads(data)]
+
+ @classmethod
+ def decode_state(cls, code):
+ return cls.state.get(code,'UNKNOWN')
+
+ # used in conf mode
+ @classmethod
+ def is_running(cls):
+ if not os.path.exists(cls.location['pid']):
+ return False
+ return util.process_running(cls.location['pid'])
+
+ @classmethod
+ def collect(cls, what):
+ fname = cls.location[what]
+ try:
+ # send signal to generate the configuration file
+ pid = util.read_file(cls.location['pid'])
+ os.kill(int(pid), cls._signal[what])
+
+ # should look for file size change?
+ sleep(0.2)
+ return util.read_file(fname)
+ except FileNotFoundError:
+ raise VRRPNoData("VRRP data is not available (process not running or no active groups)")
+ except Exception:
+ name = cls._name[what]
+ raise VRRPError(f'VRRP {name} is not available')
+ finally:
+ if os.path.exists(fname):
+ os.remove(fname)
+
+ @classmethod
+ def disabled(cls):
+ if not os.path.exists(cls.location['vyos']):
+ return []
+
+ disabled = []
+ config = json.loads(util.read_file(cls.location['vyos']))
+
+ # add disabled groups to the list
+ for group in config['vrrp_groups']:
+ if group['disable']:
+ disabled.append(
+ [group['name'], group['interface'], group['vrid'], 'DISABLED', ''])
+
+ # return list with disabled instances
+ return disabled
+
+ @classmethod
+ def format(cls, data):
+ headers = ["Name", "Interface", "VRID", "State", "Priority", "Last Transition"]
+ groups = []
+
+ data = json.loads(data)
+ for group in data:
+ data = group['data']
+
+ name = data['iname']
+ intf = data['ifp_ifname']
+ vrid = data['vrid']
+ state = cls.decode_state(data["state"])
+ priority = data['effective_priority']
+
+ since = int(time() - float(data['last_transition']))
+ last = util.seconds_to_human(since)
+
+ groups.append([name, intf, vrid, state, priority, last])
+
+ # add to the active list disabled instances
+ groups.extend(cls.disabled())
+ return(tabulate(groups, headers))
+
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
new file mode 100644
index 000000000..56ebe01d1
--- /dev/null
+++ b/python/vyos/ifconfig/vti.py
@@ -0,0 +1,31 @@
+# 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 vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class VTIIf(Interface):
+ default = {
+ 'type': 'vti',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'vti',
+ 'prefixes': ['vti', ],
+ },
+ }
diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py
new file mode 100644
index 000000000..60c178b9a
--- /dev/null
+++ b/python/vyos/ifconfig/vtun.py
@@ -0,0 +1,44 @@
+# 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 vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class VTunIf(Interface):
+ default = {
+ 'type': 'vtun',
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'openvpn',
+ 'prefixes': ['vtun', ],
+ 'bridgeable': True,
+ },
+ }
+
+ # stub this interface is created in the configure script
+
+ def _create(self):
+ # we can not create this interface as it is managed outside
+ # it requires configuring OpenVPN
+ pass
+
+ def _delete(self):
+ # we can not create this interface as it is managed outside
+ # it requires configuring OpenVPN
+ pass
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
new file mode 100644
index 000000000..18a500336
--- /dev/null
+++ b/python/vyos/ifconfig/vxlan.py
@@ -0,0 +1,130 @@
+# Copyright 2019 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 copy import deepcopy
+
+from vyos import ConfigError
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class VXLANIf(Interface):
+ """
+ The VXLAN protocol is a tunnelling protocol designed to solve the
+ problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the
+ size of the identifier is expanded to 24 bits (16777216).
+
+ VXLAN is described by IETF RFC 7348, and has been implemented by a
+ number of vendors. The protocol runs over UDP using a single
+ destination port. This document describes the Linux kernel tunnel
+ device, there is also a separate implementation of VXLAN for
+ Openvswitch.
+
+ Unlike most tunnels, a VXLAN is a 1 to N network, not just point to
+ point. A VXLAN device can learn the IP address of the other endpoint
+ either dynamically in a manner similar to a learning bridge, or make
+ use of statically-configured forwarding entries.
+
+ For more information please refer to:
+ https://www.kernel.org/doc/Documentation/networking/vxlan.txt
+ """
+
+ default = {
+ 'type': 'vxlan',
+ 'group': '',
+ 'port': 8472, # The Linux implementation of VXLAN pre-dates
+ # the IANA's selection of a standard destination port
+ 'remote': '',
+ 'source_address': '',
+ 'source_interface': '',
+ 'vni': 0
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'vxlan',
+ 'prefixes': ['vxlan', ],
+ 'bridgeable': True,
+ }
+ }
+ options = Interface.options + \
+ ['group', 'remote', 'source_interface', 'port', 'vni', 'source_address']
+
+ mapping = {
+ 'ifname': 'add',
+ 'vni': 'id',
+ 'port': 'dstport',
+ 'source_address': 'local',
+ 'source_interface': 'dev',
+ }
+
+ def _create(self):
+ cmdline = ['ifname', 'type', 'vni', 'port']
+
+ if self.config['source_address']:
+ cmdline.append('source_address')
+
+ if self.config['remote']:
+ cmdline.append('remote')
+
+ if self.config['group'] or self.config['source_interface']:
+ if self.config['group'] and self.config['source_interface']:
+ cmdline.append('group')
+ cmdline.append('source_interface')
+ else:
+ ifname = self.config['ifname']
+ raise ConfigError(
+ f'VXLAN "{ifname}" is missing mandatory underlay multicast'
+ 'group or source interface for a multicast network.')
+
+ cmd = 'ip link'
+ for key in cmdline:
+ value = self.config.get(key, '')
+ if not value:
+ continue
+ cmd += ' {} {}'.format(self.mapping.get(key, key), value)
+
+ self._cmd(cmd)
+
+ @classmethod
+ def get_config(cls):
+ """
+ VXLAN interfaces require a configuration when they are added using
+ iproute2. This static method will provide the configuration dictionary
+ used by this class.
+
+ Example:
+ >> dict = VXLANIf().get_config()
+ """
+ return deepcopy(cls.default)
+
+ 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. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
new file mode 100644
index 000000000..fad4ef282
--- /dev/null
+++ b/python/vyos/ifconfig/wireguard.py
@@ -0,0 +1,247 @@
+# Copyright 2019 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 time
+from datetime import timedelta
+
+from hurry.filesize import size
+from hurry.filesize import alternative
+
+from vyos.config import Config
+from vyos.ifconfig import Interface
+from vyos.ifconfig import Operational
+from vyos.validate import is_ipv6
+
+class WireGuardOperational(Operational):
+ def _dump(self):
+ """Dump wireguard data in a python friendly way."""
+ last_device = None
+ output = {}
+
+ # Dump wireguard connection data
+ _f = self._cmd('wg show all dump')
+ for line in _f.split('\n'):
+ if not line:
+ # Skip empty lines and last line
+ continue
+ items = line.split('\t')
+
+ if last_device != items[0]:
+ # We are currently entering a new node
+ device, private_key, public_key, listen_port, fw_mark = items
+ last_device = device
+
+ output[device] = {
+ 'private_key': None if private_key == '(none)' else private_key,
+ 'public_key': None if public_key == '(none)' else public_key,
+ 'listen_port': int(listen_port),
+ 'fw_mark': None if fw_mark == 'off' else int(fw_mark),
+ 'peers': {},
+ }
+ else:
+ # We are entering a peer
+ device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items
+ if allowed_ips == '(none)':
+ allowed_ips = []
+ else:
+ allowed_ips = allowed_ips.split('\t')
+ output[device]['peers'][public_key] = {
+ 'preshared_key': None if preshared_key == '(none)' else preshared_key,
+ 'endpoint': None if endpoint == '(none)' else endpoint,
+ 'allowed_ips': allowed_ips,
+ 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake),
+ 'transfer_rx': int(transfer_rx),
+ 'transfer_tx': int(transfer_tx),
+ 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive),
+ }
+ return output
+
+ def show_interface(self):
+ wgdump = self._dump().get(self.config['ifname'], None)
+
+ c = Config()
+
+ c.set_level(["interfaces", "wireguard", self.config['ifname']])
+ description = c.return_effective_value(["description"])
+ ips = c.return_effective_values(["address"])
+
+ answer = "interface: {}\n".format(self.config['ifname'])
+ if (description):
+ answer += " description: {}\n".format(description)
+ if (ips):
+ answer += " address: {}\n".format(", ".join(ips))
+
+ answer += " public key: {}\n".format(wgdump['public_key'])
+ answer += " private key: (hidden)\n"
+ answer += " listening port: {}\n".format(wgdump['listen_port'])
+ answer += "\n"
+
+ for peer in c.list_effective_nodes(["peer"]):
+ if wgdump['peers']:
+ pubkey = c.return_effective_value(["peer", peer, "pubkey"])
+ if pubkey in wgdump['peers']:
+ wgpeer = wgdump['peers'][pubkey]
+
+ answer += " peer: {}\n".format(peer)
+ answer += " public key: {}\n".format(pubkey)
+
+ """ figure out if the tunnel is recently active or not """
+ status = "inactive"
+ if (wgpeer['latest_handshake'] is None):
+ """ no handshake ever """
+ status = "inactive"
+ else:
+ if int(wgpeer['latest_handshake']) > 0:
+ delta = timedelta(seconds=int(
+ time.time() - wgpeer['latest_handshake']))
+ answer += " latest handshake: {}\n".format(delta)
+ if (time.time() - int(wgpeer['latest_handshake']) < (60*5)):
+ """ Five minutes and the tunnel is still active """
+ status = "active"
+ else:
+ """ it's been longer than 5 minutes """
+ status = "inactive"
+ elif int(wgpeer['latest_handshake']) == 0:
+ """ no handshake ever """
+ status = "inactive"
+ answer += " status: {}\n".format(status)
+
+ if wgpeer['endpoint'] is not None:
+ answer += " endpoint: {}\n".format(wgpeer['endpoint'])
+
+ if wgpeer['allowed_ips'] is not None:
+ answer += " allowed ips: {}\n".format(
+ ",".join(wgpeer['allowed_ips']).replace(",", ", "))
+
+ if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0:
+ rx_size = size(
+ wgpeer['transfer_rx'], system=alternative)
+ tx_size = size(
+ wgpeer['transfer_tx'], system=alternative)
+ answer += " transfer: {} received, {} sent\n".format(
+ rx_size, tx_size)
+
+ if wgpeer['persistent_keepalive'] is not None:
+ answer += " persistent keepalive: every {} seconds\n".format(
+ wgpeer['persistent_keepalive'])
+ answer += '\n'
+ return answer + super().formated_stats()
+
+
+@Interface.register
+class WireGuardIf(Interface):
+ OperationalClass = WireGuardOperational
+
+ default = {
+ 'type': 'wireguard',
+ 'port': 0,
+ 'private_key': None,
+ 'pubkey': None,
+ 'psk': '',
+ 'allowed_ips': [],
+ 'fwmark': 0x00,
+ 'endpoint': None,
+ 'keepalive': 0
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'wireguard',
+ 'prefixes': ['wg', ],
+ 'bridgeable': True,
+ }
+ }
+ options = Interface.options + \
+ ['port', 'private_key', 'pubkey', 'psk',
+ 'allowed_ips', 'fwmark', 'endpoint', 'keepalive']
+
+ 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. """
+
+ # remove no longer associated peers first
+ if 'peer_remove' in config:
+ for tmp in config['peer_remove']:
+ peer = config['peer_remove'][tmp]
+ peer['ifname'] = config['ifname']
+
+ cmd = 'wg set {ifname} peer {pubkey} remove'
+ self._cmd(cmd.format(**peer))
+
+ # Wireguard base command is identical for every peer
+ base_cmd = 'wg set {ifname} private-key {private_key}'
+ if 'port' in config:
+ base_cmd += ' listen-port {port}'
+ if 'fwmark' in config:
+ base_cmd += ' fwmark {fwmark}'
+
+ base_cmd = base_cmd.format(**config)
+
+ for tmp in config['peer']:
+ peer = config['peer'][tmp]
+
+ # start of with a fresh 'wg' command
+ cmd = base_cmd + ' peer {pubkey}'
+
+ # If no PSK is given remove it by using /dev/null - passing keys via
+ # the shell (usually bash) is considered insecure, thus we use a file
+ no_psk_file = '/dev/null'
+ psk_file = no_psk_file
+ if 'preshared_key' in peer:
+ psk_file = '/tmp/tmp.wireguard.psk'
+ with open(psk_file, 'w') as f:
+ f.write(peer['preshared_key'])
+ cmd += f' preshared-key {psk_file}'
+
+ # Persistent keepalive is optional
+ if 'persistent_keepalive'in peer:
+ cmd += ' persistent-keepalive {persistent_keepalive}'
+
+ # Multiple allowed-ip ranges can be defined - ensure we are always
+ # dealing with a list
+ if isinstance(peer['allowed_ips'], str):
+ peer['allowed_ips'] = [peer['allowed_ips']]
+ cmd += ' allowed-ips ' + ','.join(peer['allowed_ips'])
+
+ # Endpoint configuration is optional
+ if {'address', 'port'} <= set(peer):
+ if is_ipv6(config['address']):
+ cmd += ' endpoint [{address}]:{port}'
+ else:
+ cmd += ' endpoint {address}:{port}'
+
+ self._cmd(cmd.format(**peer))
+
+ # PSK key file is not required to be stored persistently as its backed by CLI
+ if psk_file != no_psk_file and os.path.exists(psk_file):
+ os.remove(psk_file)
+
+ # call base class
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
+
diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py
new file mode 100644
index 000000000..a50346ffa
--- /dev/null
+++ b/python/vyos/ifconfig/wireless.py
@@ -0,0 +1,102 @@
+# 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
+
+from vyos.ifconfig.interface import Interface
+from vyos.ifconfig.vlan import VLAN
+
+
+@Interface.register
+@VLAN.enable
+class WiFiIf(Interface):
+ """
+ Handle WIFI/WLAN interfaces.
+ """
+
+ default = {
+ 'type': 'wifi',
+ 'phy': 'phy0'
+ }
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'wireless',
+ 'prefixes': ['wlan', ],
+ 'bridgeable': True,
+ }
+ }
+ options = Interface.options + \
+ ['phy', 'op_mode']
+
+ def _create(self):
+ # all interfaces will be added in monitor mode
+ cmd = 'iw phy {phy} interface add {ifname} type monitor' \
+ .format(**self.config)
+ self._cmd(cmd)
+
+ # wireless interface is administratively down by default
+ self.set_admin_state('down')
+
+ def _delete(self):
+ cmd = 'iw dev {ifname} del' \
+ .format(**self.config)
+ self._cmd(cmd)
+
+ @staticmethod
+ def get_config():
+ """
+ WiFi interfaces require a configuration when they are added using
+ iw (type/phy). This static method will provide the configuration
+ ictionary used by this class.
+
+ Example:
+ >> conf = WiFiIf().get_config()
+ """
+ config = {
+ 'phy': 'phy0'
+ }
+ return config
+
+
+ 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. """
+
+ # call base class first
+ super().update(config)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
+
+
+@Interface.register
+class WiFiModemIf(WiFiIf):
+ definition = {
+ **WiFiIf.definition,
+ **{
+ 'section': 'wirelessmodem',
+ 'prefixes': ['wlm', ],
+ }
+ }
diff --git a/python/vyos/iflag.py b/python/vyos/iflag.py
new file mode 100644
index 000000000..7ff8e5623
--- /dev/null
+++ b/python/vyos/iflag.py
@@ -0,0 +1,38 @@
+# Copyright 2019 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 Enum, unique, IntEnum
+
+
+class IFlag(IntEnum):
+ """ net/if.h interface flags """
+
+ IFF_UP = 0x1 #: Interface up/down status
+ IFF_BROADCAST = 0x2 #: Broadcast address valid
+ IFF_DEBUG = 0x4, #: Debugging
+ IFF_LOOPBACK = 0x8 #: Is loopback network
+ IFF_POINTOPOINT = 0x10 #: Is point-to-point link
+ IFF_NOTRAILERS = 0x20 #: Avoid use of trailers
+ IFF_RUNNING = 0x40 #: Resources allocated
+ IFF_NOARP = 0x80 #: No address resolution protocol
+ IFF_PROMISC = 0x100 #: Promiscuous mode
+ IFF_ALLMULTI = 0x200 #: Receive all multicast
+ IFF_MASTER = 0x400 #: Load balancer master
+ IFF_SLAVE = 0x800 #: Load balancer slave
+ IFF_MULTICAST = 0x1000 #: Supports multicast
+ IFF_PORTSEL = 0x2000 #: Media type adjustable
+ IFF_AUTOMEDIA = 0x4000 #: Automatic media type enabled
+ IFF_DYNAMIC = 0x8000 #: Is a dial-up device with dynamic address
+
diff --git a/python/vyos/initialsetup.py b/python/vyos/initialsetup.py
new file mode 100644
index 000000000..574e7892d
--- /dev/null
+++ b/python/vyos/initialsetup.py
@@ -0,0 +1,72 @@
+# initialsetup -- functions for setting common values in config file,
+# for use in installation and first boot scripts
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import vyos.configtree
+import vyos.authutils
+
+def set_interface_address(config, intf, addr, intf_type="ethernet"):
+ config.set(["interfaces", intf_type, intf, "address"], value=addr)
+ config.set_tag(["interfaces", intf_type])
+
+def set_host_name(config, hostname):
+ config.set(["system", "host-name"], value=hostname)
+
+def set_name_servers(config, servers):
+ for s in servers:
+ config.set(["system", "name-server"], replace=False, value=s)
+
+def set_default_gateway(config, gateway):
+ config.set(["protocols", "static", "route", "0.0.0.0/0", "next-hop", gateway])
+ config.set_tag(["protocols", "static", "route"])
+ config.set_tag(["protocols", "static", "route", "0.0.0.0/0", "next-hop"])
+
+def set_user_password(config, user, password):
+ # Make a password hash
+ hash = vyos.authutils.make_password_hash(password)
+
+ config.set(["system", "login", "user", user, "authentication", "encrypted-password"], value=hash)
+ config.set(["system", "login", "user", user, "authentication", "plaintext-password"], value="")
+
+def disable_user_password(config, user):
+ config.set(["system", "login", "user", user, "authentication", "encrypted-password"], value="!")
+ config.set(["system", "login", "user", user, "authentication", "plaintext-password"], value="")
+
+def set_user_level(config, user, level):
+ config.set(["system", "login", "user", user, "level"], value=level)
+
+def set_user_ssh_key(config, user, key_string):
+ key = vyos.authutils.split_ssh_public_key(key_string, defaultname=user)
+
+ config.set(["system", "login", "user", user, "authentication", "public-keys", key["name"], "key"], value=key["data"])
+ config.set(["system", "login", "user", user, "authentication", "public-keys", key["name"], "type"], value=key["type"])
+ config.set_tag(["system", "login", "user", user, "authentication", "public-keys"])
+
+def create_user(config, user, password=None, key=None, level="admin"):
+ config.set(["system", "login", "user", user])
+ config.set_tag(["system", "login", "user", user])
+
+ if not key and not password:
+ raise ValueError("Must set at least password or SSH public key")
+
+ if password:
+ set_user_password(config, user, password)
+ else:
+ disable_user_password(config, user)
+
+ if key:
+ set_user_ssh_key(config, user, key)
+
+ set_user_level(config, user, level)
diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py
new file mode 100644
index 000000000..cfa75aac6
--- /dev/null
+++ b/python/vyos/ioctl.py
@@ -0,0 +1,36 @@
+# Copyright 2019 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 sys
+import os
+import socket
+import fcntl
+import struct
+
+SIOCGIFFLAGS = 0x8913
+
+def get_terminal_size():
+ """ pull the terminal size """
+ """ rows,cols = vyos.ioctl.get_terminal_size() """
+ columns, rows = os.get_terminal_size(0)
+ return (rows,columns)
+
+def get_interface_flags(intf):
+ """ Pull the SIOCGIFFLAGS """
+ nullif = '\0'*256
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ raw = fcntl.ioctl(sock.fileno(), SIOCGIFFLAGS, intf + nullif)
+ flags, = struct.unpack('H', raw[16:18])
+ return flags
diff --git a/python/vyos/limericks.py b/python/vyos/limericks.py
new file mode 100644
index 000000000..e03ccd32b
--- /dev/null
+++ b/python/vyos/limericks.py
@@ -0,0 +1,72 @@
+# Copyright 2015, 2018 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 random
+
+limericks = [
+
+"""
+A programmer who's name was Searle
+Once wrote a long program in Perl.
+Despite very few quirks
+No one got how it works,
+Not even the interpreter.
+""",
+
+"""
+There was a young lady of Maine
+Who set up IPsec VPN.
+Problems didn't arise
+'til other vendors' device
+had to add she to that VPN.
+""",
+
+"""
+One day a programmer from York
+started his own Vyatta fork.
+Though he was a huge geek,
+it still took him a week
+to get the damn build scripts to work.
+""",
+
+"""
+A network admin from Hong Kong
+knew MPPE cipher's not strong.
+But he was behind NAT,
+so he put up we that,
+sad network admin from Hong Kong.
+""",
+
+"""
+A network admin named Drake
+greeted friends with a three-way handshake
+and refused to proceed
+if they didn't complete it,
+that standards-compliant guy Drake.
+""",
+
+"""
+A network admin from Nantucket
+used hierarchy token buckets.
+Bandwidth limits he set
+slowed down his net,
+users drove him away from Nantucket.
+"""
+
+]
+
+
+def get_random():
+ return limericks[random.randint(0, len(limericks) - 1)]
diff --git a/python/vyos/logger.py b/python/vyos/logger.py
new file mode 100644
index 000000000..f7cc964d5
--- /dev/null
+++ b/python/vyos/logger.py
@@ -0,0 +1,143 @@
+# 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/>.
+
+# A wrapper class around logging to make it easier to use
+
+# for a syslog logger:
+# from vyos.logger import syslog
+# syslog.critical('message')
+
+# for a stderr logger:
+# from vyos.logger import stderr
+# stderr.critical('message')
+
+# for a custom logger (syslog and file):
+# from vyos.logger import getLogger
+# combined = getLogger(__name__, syslog=True, stream=sys.stdout, filename='/tmp/test')
+# combined.critical('message')
+
+import sys
+import logging
+import logging.handlers as handlers
+
+TIMED = '%(asctime)s: %(message)s'
+SHORT = '%(filename)s: %(message)s'
+CLEAR = '%(levelname) %(asctime)s %(filename)s: %(message)s'
+
+_levels = {
+ 'CRITICAL': logging.CRITICAL,
+ 'ERROR': logging.CRITICAL,
+ 'WARNING': logging.WARNING,
+ 'INFO': logging.INFO,
+ 'DEBUG': logging.DEBUG,
+ 'NOTSET': logging.NOTSET,
+}
+
+# prevent recreation of already created logger
+_created = {}
+
+def getLogger(name=None, **kwargs):
+ if name in _created:
+ if len(kwargs) == 0:
+ return _created[name]
+ raise ValueError('a logger with the name "{name} already exists')
+
+ logger = logging.getLogger(name)
+ logger.setLevel(_levels[kwargs.get('level', 'DEBUG')])
+
+ if 'address' in kwargs or kwargs.get('syslog', False):
+ logger.addHandler(_syslog(**kwargs))
+ if 'stream' in kwargs:
+ logger.addHandler(_stream(**kwargs))
+ if 'filename' in kwargs:
+ logger.addHandler(_file(**kwargs))
+
+ _created[name] = logger
+ return logger
+
+
+def _syslog(**kwargs):
+ formating = kwargs.get('format', SHORT)
+ handler = handlers.SysLogHandler(
+ address=kwargs.get('address', '/dev/log'),
+ facility=kwargs.get('facility', 'syslog'),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+def _stream(**kwargs):
+ formating = kwargs.get('format', CLEAR)
+ handler = logging.StreamHandler(
+ stream=kwargs.get('stream', sys.stderr),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+def _file(**kwargs):
+ formating = kwargs.get('format', CLEAR)
+ handler = handlers.RotatingFileHandler(
+ filename=kwargs.get('filename', 1048576),
+ maxBytes=kwargs.get('maxBytes', 1048576),
+ backupCount=kwargs.get('backupCount', 3),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+# exported pre-built logger, please keep in mind that the names
+# must be unique otherwise the logger are shared
+
+# a logger for stderr
+stderr = getLogger(
+ 'VyOS Syslog',
+ format=SHORT,
+ stream=sys.stderr,
+ address='/dev/log'
+)
+
+# a logger to syslog
+syslog = getLogger(
+ 'VyOS StdErr',
+ format='%(message)s',
+ address='/dev/log'
+)
+
+
+# testing
+if __name__ == '__main__':
+ # from vyos.logger import getLogger
+ formating = '%(asctime)s (%(filename)s) %(levelname)s: %(message)s'
+
+ # syslog logger
+ # syslog=True if no 'address' field is provided
+ syslog = getLogger(__name__ + '.1', syslog=True, format=formating)
+ syslog.info('syslog test')
+
+ # steam logger
+ stream = getLogger(__name__ + '.2', stream=sys.stdout, level='ERROR')
+ stream.info('steam test')
+
+ # file logger
+ filelog = getLogger(__name__ + '.3', filename='/tmp/test')
+ filelog.info('file test')
+
+ # create a combined logger
+ getLogger('VyOS', syslog=True, stream=sys.stdout, filename='/tmp/test')
+
+ # recover the created logger from name
+ combined = getLogger('VyOS')
+ combined.info('combined test')
diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
new file mode 100644
index 000000000..9a5fdef2f
--- /dev/null
+++ b/python/vyos/migrator.py
@@ -0,0 +1,220 @@
+# Copyright 2019 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 sys
+import os
+import subprocess
+import vyos.version
+import vyos.defaults
+import vyos.systemversions as systemversions
+import vyos.formatversions as formatversions
+
+class MigratorError(Exception):
+ pass
+
+class Migrator(object):
+ def __init__(self, config_file, force=False, set_vintage='vyos'):
+ self._config_file = config_file
+ self._force = force
+ self._set_vintage = set_vintage
+ self._config_file_vintage = None
+ self._log_file = None
+ self._changed = False
+
+ def read_config_file_versions(self):
+ """
+ Get component versions from config file footer and set vintage;
+ return empty dictionary if config string is missing.
+ """
+ cfg_file = self._config_file
+ component_versions = {}
+
+ cfg_versions = formatversions.read_vyatta_versions(cfg_file)
+
+ if cfg_versions:
+ self._config_file_vintage = 'vyatta'
+ component_versions = cfg_versions
+
+ cfg_versions = formatversions.read_vyos_versions(cfg_file)
+
+ if cfg_versions:
+ self._config_file_vintage = 'vyos'
+ component_versions = cfg_versions
+
+ return component_versions
+
+ def update_vintage(self):
+ old_vintage = self._config_file_vintage
+
+ if self._set_vintage:
+ self._config_file_vintage = self._set_vintage
+
+ if self._config_file_vintage not in ['vyatta', 'vyos']:
+ raise MigratorError("Unknown vintage.")
+
+ if self._config_file_vintage == old_vintage:
+ return False
+ else:
+ return True
+
+ def open_log_file(self):
+ """
+ Open log file for migration, catching any error.
+ Note that, on boot, migration takes place before the canonical log
+ directory is created, hence write to the config file directory.
+ """
+ self._log_file = os.path.join(vyos.defaults.directories['config'],
+ 'vyos-migrate.log')
+ # on creation, allow write permission for cfg_group;
+ # restore original umask on exit
+ mask = os.umask(0o113)
+ try:
+ log = open('{0}'.format(self._log_file), 'w')
+ log.write("List of executed migration scripts:\n")
+ except Exception as e:
+ os.umask(mask)
+ print("Logging error: {0}".format(e))
+ return None
+
+ os.umask(mask)
+ return log
+
+ def run_migration_scripts(self, config_file_versions, system_versions):
+ """
+ Run migration scripts iteratively, until config file version equals
+ system component version.
+ """
+ log = self.open_log_file()
+
+ cfg_versions = config_file_versions
+ sys_versions = system_versions
+
+ sys_keys = list(sys_versions.keys())
+ sys_keys.sort()
+
+ rev_versions = {}
+
+ for key in sys_keys:
+ sys_ver = sys_versions[key]
+ if key in cfg_versions:
+ cfg_ver = cfg_versions[key]
+ else:
+ cfg_ver = 0
+
+ migrate_script_dir = os.path.join(
+ vyos.defaults.directories['migrate'], key)
+
+ while cfg_ver < sys_ver:
+ next_ver = cfg_ver + 1
+
+ migrate_script = os.path.join(migrate_script_dir,
+ '{}-to-{}'.format(cfg_ver, next_ver))
+
+ try:
+ subprocess.check_call([migrate_script,
+ self._config_file])
+ except FileNotFoundError:
+ pass
+ except Exception as err:
+ print("\nMigration script error: {0}: {1}."
+ "".format(migrate_script, err))
+ sys.exit(1)
+
+ if log:
+ try:
+ log.write('{0}\n'.format(migrate_script))
+ except Exception as e:
+ print("Error writing log: {0}".format(e))
+
+ cfg_ver = next_ver
+
+ rev_versions[key] = cfg_ver
+
+ if log:
+ log.close()
+
+ return rev_versions
+
+ def write_config_file_versions(self, cfg_versions):
+ """
+ Write new versions string.
+ """
+ versions_string = formatversions.format_versions_string(cfg_versions)
+
+ os_version_string = vyos.version.get_version()
+
+ if self._config_file_vintage == 'vyatta':
+ formatversions.write_vyatta_versions_foot(self._config_file,
+ versions_string,
+ os_version_string)
+
+ if self._config_file_vintage == 'vyos':
+ formatversions.write_vyos_versions_foot(self._config_file,
+ versions_string,
+ os_version_string)
+
+ def run(self):
+ """
+ Gather component versions from config file and system.
+ Run migration scripts.
+ Update vintage ('vyatta' or 'vyos'), if needed.
+ If changed, remove old versions string from config file, and
+ write new versions string.
+ """
+ cfg_file = self._config_file
+
+ cfg_versions = self.read_config_file_versions()
+ if self._force:
+ # This will force calling all migration scripts:
+ cfg_versions = {}
+
+ sys_versions = systemversions.get_system_versions()
+
+ rev_versions = self.run_migration_scripts(cfg_versions, sys_versions)
+
+ if rev_versions != cfg_versions:
+ self._changed = True
+
+ if self.update_vintage():
+ self._changed = True
+
+ if not self._changed:
+ return
+
+ formatversions.remove_versions(cfg_file)
+
+ self.write_config_file_versions(rev_versions)
+
+ def config_changed(self):
+ return self._changed
+
+class VirtualMigrator(Migrator):
+ def run(self):
+ cfg_file = self._config_file
+
+ cfg_versions = self.read_config_file_versions()
+ if not cfg_versions:
+ return
+
+ if self.update_vintage():
+ self._changed = True
+
+ if not self._changed:
+ return
+
+ formatversions.remove_versions(cfg_file)
+
+ self.write_config_file_versions(cfg_versions)
+
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
new file mode 100644
index 000000000..3f46d979b
--- /dev/null
+++ b/python/vyos/remote.py
@@ -0,0 +1,143 @@
+# Copyright 2019 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 sys
+import os
+import re
+import fileinput
+
+from vyos.util import cmd
+from vyos.util import DEVNULL
+
+
+def check_and_add_host_key(host_name):
+ """
+ Filter host keys and prompt for adding key to known_hosts file, if
+ needed.
+ """
+ known_hosts = '{}/.ssh/known_hosts'.format(os.getenv('HOME'))
+ if not os.path.exists(known_hosts):
+ mode = 0o600
+ os.mknod(known_hosts, 0o600)
+
+ keyscan_cmd = 'ssh-keyscan -t rsa {}'.format(host_name)
+
+ try:
+ host_key = cmd(keyscan_cmd, stderr=DEVNULL)
+ except OSError:
+ sys.exit("Can not get RSA host key")
+
+ # libssh2 (jessie; stretch) does not recognize ec host keys, and curl
+ # will fail with error 51 if present in known_hosts file; limit to rsa.
+ usable_keys = False
+ offending_keys = []
+ for line in fileinput.input(known_hosts, inplace=True):
+ if host_name in line and 'ssh-rsa' in line:
+ if line.split()[-1] != host_key.split()[-1]:
+ offending_keys.append(line)
+ continue
+ else:
+ usable_keys = True
+ if host_name in line and not 'ssh-rsa' in line:
+ continue
+
+ sys.stdout.write(line)
+
+ if usable_keys:
+ return
+
+ if offending_keys:
+ print("Host key has changed!")
+ print("If you trust the host key fingerprint below, continue.")
+
+ fingerprint_cmd = 'ssh-keygen -lf /dev/stdin'
+ try:
+ fingerprint = cmd(fingerprint_cmd, stderr=DEVNULL, input=host_key)
+ except OSError:
+ sys.exit("Can not get RSA host key fingerprint.")
+
+ print("RSA host key fingerprint is {}".format(fingerprint.split()[1]))
+ response = input("Do you trust this host? [y]/n ")
+
+ if not response or response == 'y':
+ with open(known_hosts, 'a+') as f:
+ print("Adding {} to the list of known"
+ " hosts.".format(host_name))
+ f.write(host_key)
+ else:
+ sys.exit("Host not trusted")
+
+def get_remote_config(remote_file):
+ """ Invoke curl to download remote (config) file.
+
+ Args:
+ remote file URI:
+ scp://<user>[:<passwd>]@<host>/<file>
+ sftp://<user>[:<passwd>]@<host>/<file>
+ http://<host>/<file>
+ https://<host>/<file>
+ ftp://<user>[:<passwd>]@<host>/<file>
+ tftp://<host>/<file>
+ """
+ request = dict.fromkeys(['protocol', 'user', 'host', 'file'])
+ protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp']
+ or_protocols = '|'.join(protocols)
+
+ request_match = re.match(r'(' + or_protocols + r')://(.*?)(/.*)',
+ remote_file)
+ if request_match:
+ (request['protocol'], request['host'],
+ request['file']) = request_match.groups()
+ else:
+ print("Malformed URI")
+ sys.exit(1)
+
+ user_match = re.search(r'(.*)@(.*)', request['host'])
+ if user_match:
+ request['user'] = user_match.groups()[0]
+ request['host'] = user_match.groups()[1]
+
+ remote_file = '{0}://{1}{2}'.format(request['protocol'], request['host'], request['file'])
+
+ if request['protocol'] in ('scp', 'sftp'):
+ check_and_add_host_key(request['host'])
+
+ redirect_opt = ''
+
+ if request['protocol'] in ('http', 'https'):
+ redirect_opt = '-L'
+ # Try header first, and look for 'OK' or 'Moved' codes:
+ curl_cmd = 'curl {0} -q -I {1}'.format(redirect_opt, remote_file)
+ try:
+ curl_output = cmd(curl_cmd)
+ except OSError:
+ sys.exit(1)
+
+ return_vals = re.findall(r'^HTTP\/\d+\.?\d\s+(\d+)\s+(.*)$',
+ curl_output, re.MULTILINE)
+ for val in return_vals:
+ if int(val[0]) not in [200, 301, 302]:
+ print('HTTP error: {0} {1}'.format(*val))
+ sys.exit(1)
+
+ if request['user']:
+ curl_cmd = 'curl -# -u {0} {1}'.format(request['user'], remote_file)
+ else:
+ curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file)
+
+ try:
+ return cmd(curl_cmd, stderr=None)
+ except OSError:
+ return None
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/systemversions.py b/python/vyos/systemversions.py
new file mode 100644
index 000000000..5c4deca29
--- /dev/null
+++ b/python/vyos/systemversions.py
@@ -0,0 +1,63 @@
+# Copyright 2019 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 sys
+import json
+
+import vyos.defaults
+
+def get_system_versions():
+ """
+ Get component versions from running system: read vyatta directory
+ structure for versions, then read vyos JSON file. It is a critical
+ error if either migration directory or JSON file is unreadable.
+ """
+ system_versions = {}
+
+ try:
+ version_info = os.listdir(vyos.defaults.directories['current'])
+ except OSError as err:
+ print("OS error: {}".format(err))
+ sys.exit(1)
+
+ for info in version_info:
+ if re.match(r'[\w,-]+@\d+', info):
+ pair = info.split('@')
+ system_versions[pair[0]] = int(pair[1])
+
+ version_dict = {}
+ path = vyos.defaults.version_file
+
+ if os.path.isfile(path):
+ with open(path, 'r') as f:
+ try:
+ version_dict = json.load(f)
+ except ValueError as err:
+ print(f"\nValue error in {path}: {err}")
+ sys.exit(1)
+
+ for k, v in version_dict.items():
+ if not isinstance(v, int):
+ print(f"\nType error in {path}; expecting Dict[str, int]")
+ sys.exit(1)
+ existing = system_versions.get(k)
+ if existing is None:
+ system_versions[k] = v
+ elif v > existing:
+ system_versions[k] = v
+
+ return system_versions
diff --git a/python/vyos/template.py b/python/vyos/template.py
new file mode 100644
index 000000000..c88ab04a0
--- /dev/null
+++ b/python/vyos/template.py
@@ -0,0 +1,143 @@
+# Copyright 2019-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 functools
+import os
+
+from ipaddress import ip_network
+from jinja2 import Environment
+from jinja2 import FileSystemLoader
+from vyos.defaults import directories
+from vyos.util import chmod, chown, makedir
+
+
+# Holds template filters registered via register_filter()
+_FILTERS = {}
+
+
+# reuse Environments with identical trim_blocks setting to improve performance
+@functools.lru_cache(maxsize=2)
+def _get_environment(trim_blocks):
+ env = Environment(
+ # Don't check if template files were modified upon re-rendering
+ auto_reload=False,
+ # Cache up to this number of templates for quick re-rendering
+ cache_size=100,
+ loader=FileSystemLoader(directories["templates"]),
+ trim_blocks=trim_blocks,
+ )
+ env.filters.update(_FILTERS)
+ return env
+
+
+def register_filter(name, func=None):
+ """Register a function to be available as filter in templates under given name.
+
+ It can also be used as a decorator, see below in this module for examples.
+
+ :raise RuntimeError:
+ when trying to register a filter after a template has been rendered already
+ :raise ValueError: when trying to register a name which was taken already
+ """
+ if func is None:
+ return functools.partial(register_filter, name)
+ if _get_environment.cache_info().currsize:
+ raise RuntimeError(
+ "Filters can only be registered before rendering the first template"
+ )
+ if name in _FILTERS:
+ raise ValueError(f"A filter with name {name!r} was registered already")
+ _FILTERS[name] = func
+ return func
+
+
+def render_to_string(template, content, trim_blocks=False, formater=None):
+ """Render a template from the template directory, raise on any errors.
+
+ :param template: the path to the template relative to the template folder
+ :param content: the dictionary of variables to put into rendering context
+ :param trim_blocks: controls the trim_blocks jinja2 feature
+ :param formater:
+ if given, it has to be a callable the rendered string is passed through
+
+ The parsed template files are cached, so rendering the same file multiple times
+ does not cause as too much overhead.
+ If used everywhere, it could be changed to load the template from Python
+ environment variables from an importable Python module generated when the Debian
+ package is build (recovering the load time and overhead caused by having the
+ file out of the code).
+ """
+ template = _get_environment(bool(trim_blocks)).get_template(template)
+ rendered = template.render(content)
+ if formater is not None:
+ rendered = formater(rendered)
+ return rendered
+
+
+def render(
+ destination,
+ template,
+ content,
+ trim_blocks=False,
+ formater=None,
+ permission=None,
+ user=None,
+ group=None,
+):
+ """Render a template from the template directory to a file, raise on any errors.
+
+ :param destination: path to the file to save the rendered template in
+ :param permission: permission bitmask to set for the output file
+ :param user: user to own the output file
+ :param group: group to own the output file
+
+ All other parameters are as for :func:`render_to_string`.
+ """
+ # Create the directory if it does not exist
+ folder = os.path.dirname(destination)
+ makedir(folder, user, group)
+
+ # As we are opening the file with 'w', we are performing the rendering before
+ # calling open() to not accidentally erase the file if rendering fails
+ rendered = render_to_string(template, content, trim_blocks, formater)
+
+ # Write to file
+ with open(destination, "w") as file:
+ chmod(file.fileno(), permission)
+ chown(file.fileno(), user, group)
+ file.write(rendered)
+
+
+##################################
+# Custom template filters follow #
+##################################
+
+
+@register_filter("address_from_cidr")
+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::
+ """
+ return ip_network(text).network_address
+
+
+@register_filter("netmask_from_cidr")
+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::
+ """
+ return ip_network(text).netmask
diff --git a/python/vyos/util.py b/python/vyos/util.py
new file mode 100644
index 000000000..84aa16791
--- /dev/null
+++ b/python/vyos/util.py
@@ -0,0 +1,688 @@
+# 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 sys
+
+#
+# NOTE: Do not import full classes here, move your import to the function
+# where it is used so it is as local as possible to the execution
+#
+
+
+def _need_sudo(command):
+ return os.path.basename(command.split()[0]) in ('systemctl', )
+
+
+def _add_sudo(command):
+ if _need_sudo(command):
+ return 'sudo ' + command
+ return command
+
+
+from subprocess import Popen
+from subprocess import PIPE
+from subprocess import STDOUT
+from subprocess import DEVNULL
+
+
+def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True):
+ """
+ popen is a wrapper helper aound subprocess.Popen
+ with it default setting it will return a tuple (out, err)
+ out: the output of the program run
+ err: the error code returned by the program
+
+ it can be affected by the following flags:
+ shell: do not try to auto-detect if a shell is required
+ for example if a pipe (|) or redirection (>, >>) is used
+ input: data to sent to the child process via STDIN
+ the data should be bytes but string will be converted
+ timeout: time after which the command will be considered to have failed
+ env: mapping that defines the environment variables for the new process
+ stdout: define how the output of the program should be handled
+ - PIPE (default), sends stdout to the output
+ - DEVNULL, discard the output
+ stderr: define how the output of the program should be handled
+ - None (default), send/merge the data to/with stderr
+ - PIPE, popen will append it to output
+ - STDOUT, send the data to be merged with stdout
+ - DEVNULL, discard the output
+ decode: specify the expected text encoding (utf-8, ascii, ...)
+ the default is explicitely utf-8 which is python's own default
+
+ usage:
+ get both stdout and stderr: popen('command', stdout=PIPE, stderr=STDOUT)
+ discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE)
+ """
+
+ # airbag must be left as an import in the function as otherwise we have a
+ # a circual import dependency
+ from vyos import debug
+ from vyos import airbag
+
+ # log if the flag is set, otherwise log if command is set
+ if not debug.enabled(flag):
+ flag = 'command'
+
+ if autosudo:
+ command = _add_sudo(command)
+
+ cmd_msg = f"cmd '{command}'"
+ debug.message(cmd_msg, flag)
+
+ use_shell = shell
+ stdin = None
+ if shell is None:
+ use_shell = False
+ if ' ' in command:
+ use_shell = True
+ if env:
+ use_shell = True
+
+ if input:
+ stdin = PIPE
+ input = input.encode() if type(input) is str else input
+
+ p = Popen(
+ command,
+ stdin=stdin, stdout=stdout, stderr=stderr,
+ env=env, shell=use_shell,
+ )
+
+ pipe = p.communicate(input, timeout)
+
+ pipe_out = b''
+ if stdout == PIPE:
+ pipe_out = pipe[0]
+
+ pipe_err = b''
+ if stderr == PIPE:
+ pipe_err = pipe[1]
+
+ str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip()
+ str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip()
+
+ out_msg = f"returned (out):\n{str_out}"
+ if str_out:
+ debug.message(out_msg, flag)
+
+ if str_err:
+ err_msg = f"returned (err):\n{str_err}"
+ # this message will also be send to syslog via airbag
+ debug.message(err_msg, flag, destination=sys.stderr)
+
+ # should something go wrong, report this too via airbag
+ airbag.noteworthy(cmd_msg)
+ airbag.noteworthy(out_msg)
+ airbag.noteworthy(err_msg)
+
+ return str_out, p.returncode
+
+
+def run(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=DEVNULL, stderr=PIPE, decode='utf-8', autosudo=True):
+ """
+ A wrapper around popen, which discard the stdout and
+ will return the error code of a command
+ """
+ _, code = popen(
+ command, flag,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ decode=decode,
+ )
+ return code
+
+
+def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True,
+ raising=None, message='', expect=[0]):
+ """
+ A wrapper around popen, which returns the stdout and
+ will raise the error code of a command
+
+ raising: specify which call should be used when raising
+ the class should only require a string as parameter
+ (default is OSError) with the error code
+ expect: a list of error codes to consider as normal
+ """
+ decoded, code = popen(
+ command, flag,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ decode=decode,
+ )
+ if code not in expect:
+ feedback = message + '\n' if message else ''
+ feedback += f'failed to run command: {command}\n'
+ feedback += f'returned: {decoded}\n'
+ feedback += f'exit code: {code}'
+ if raising is None:
+ # error code can be recovered with .errno
+ raise OSError(code, feedback)
+ else:
+ raise raising(feedback)
+ return decoded
+
+
+def call(command, flag='', shell=None, input=None, timeout=None, env=None,
+ stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True):
+ """
+ A wrapper around popen, which print the stdout and
+ will return the error code of a command
+ """
+ out, code = popen(
+ command, flag,
+ stdout=stdout, stderr=stderr,
+ input=input, timeout=timeout,
+ env=env, shell=shell,
+ decode=decode,
+ )
+ if out:
+ print(out)
+ return code
+
+
+def read_file(fname, defaultonfailure=None):
+ """
+ read the content of a file, stripping any end characters (space, newlines)
+ should defaultonfailure be not None, it is returned on failure to read
+ """
+ try:
+ """ Read a file to string """
+ with open(fname, 'r') as f:
+ data = f.read().strip()
+ return data
+ except Exception as e:
+ if defaultonfailure is not None:
+ return defaultonfailure
+ raise e
+
+
+def read_json(fname, defaultonfailure=None):
+ """
+ read and json decode the content of a file
+ should defaultonfailure be not None, it is returned on failure to read
+ """
+ import json
+ try:
+ with open(fname, 'r') as f:
+ data = json.load(f)
+ return data
+ except Exception as e:
+ if defaultonfailure is not None:
+ return defaultonfailure
+ raise e
+
+
+def chown(path, user, group):
+ """ change file/directory owner """
+ from pwd import getpwnam
+ from grp import getgrnam
+
+ if user is None or group is None:
+ return False
+
+ # path may also be an open file descriptor
+ if not isinstance(path, int) and not os.path.exists(path):
+ return False
+
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
+ os.chown(path, uid, gid)
+ return True
+
+
+def chmod(path, bitmask):
+ # path may also be an open file descriptor
+ if not isinstance(path, int) and not os.path.exists(path):
+ return
+ if bitmask is None:
+ return
+ os.chmod(path, bitmask)
+
+
+def chmod_600(path):
+ """ make file only read/writable by owner """
+ from stat import S_IRUSR, S_IWUSR
+
+ bitmask = S_IRUSR | S_IWUSR
+ chmod(path, bitmask)
+
+
+def chmod_750(path):
+ """ make file/directory only executable to user and group """
+ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP
+
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
+ chmod(path, bitmask)
+
+
+def chmod_755(path):
+ """ make file executable by all """
+ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH
+
+ bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \
+ S_IROTH | S_IXOTH
+ chmod(path, bitmask)
+
+
+def makedir(path, user=None, group=None):
+ if os.path.exists(path):
+ return
+ os.mkdir(path)
+ chown(path, user, group)
+
+
+def colon_separated_to_dict(data_string, uniquekeys=False):
+ """ Converts a string containing newline-separated entries
+ of colon-separated key-value pairs into a dict.
+
+ Such files are common in Linux /proc filesystem
+
+ Args:
+ data_string (str): data string
+ uniquekeys (bool): whether to insist that keys are unique or not
+
+ Returns: dict
+
+ Raises:
+ ValueError: if uniquekeys=True and the data string has
+ duplicate keys.
+
+ Note:
+ If uniquekeys=True, then dict entries are always strings,
+ otherwise they are always lists of strings.
+ """
+ import re
+ key_value_re = re.compile('([^:]+)\s*\:\s*(.*)')
+
+ data_raw = re.split('\n', data_string)
+
+ data = {}
+
+ for l in data_raw:
+ l = l.strip()
+ if l:
+ match = re.match(key_value_re, l)
+ if match:
+ key = match.groups()[0].strip()
+ value = match.groups()[1].strip()
+ if key in data.keys():
+ if uniquekeys:
+ raise ValueError("Data string has duplicate keys: {0}".format(key))
+ else:
+ data[key].append(value)
+ else:
+ if uniquekeys:
+ data[key] = value
+ else:
+ data[key] = [value]
+ else:
+ pass
+
+ return data
+
+def mangle_dict_keys(data, regex, replacement):
+ """ Mangles dict keys according to a regex and replacement character.
+ Some libraries like Jinja2 do not like certain characters in dict keys.
+ This function can be used for replacing all offending characters
+ with something acceptable.
+
+ Args:
+ data (dict): Original dict to mangle
+
+ Returns: dict
+ """
+ new_dict = {}
+ for key in data.keys():
+ new_key = re.sub(regex, replacement, key)
+
+ value = data[key]
+ if isinstance(value, dict):
+ new_dict[new_key] = mangle_dict_keys(value, regex, replacement)
+ else:
+ new_dict[new_key] = value
+
+ 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
+ if not os.path.isfile(pid_file):
+ return False
+ with open(pid_file, 'r') as f:
+ pid = f.read().strip()
+ return pid_exists(int(pid))
+
+
+def process_named_running(name):
+ """ Checks if process with given name is running and returns its PID.
+ If Process is not running, return None
+ """
+ from psutil import process_iter
+ for p in process_iter():
+ if name in p.name():
+ return p.pid
+ return None
+
+
+def seconds_to_human(s, separator=""):
+ """ Converts number of seconds passed to a human-readable
+ interval such as 1w4d18h35m59s
+ """
+ s = int(s)
+
+ week = 60 * 60 * 24 * 7
+ day = 60 * 60 * 24
+ hour = 60 * 60
+
+ remainder = 0
+ result = ""
+
+ weeks = s // week
+ if weeks > 0:
+ result = "{0}w".format(weeks)
+ s = s % week
+
+ days = s // day
+ if days > 0:
+ result = "{0}{1}{2}d".format(result, separator, days)
+ s = s % day
+
+ hours = s // hour
+ if hours > 0:
+ result = "{0}{1}{2}h".format(result, separator, hours)
+ s = s % hour
+
+ minutes = s // 60
+ if minutes > 0:
+ result = "{0}{1}{2}m".format(result, separator, minutes)
+ s = s % 60
+
+ seconds = s
+ if seconds > 0:
+ result = "{0}{1}{2}s".format(result, separator, seconds)
+
+ return result
+
+
+def get_cfg_group_id():
+ from grp import getgrnam
+ from vyos.defaults import cfg_group
+
+ group_data = getgrnam(cfg_group)
+ return group_data.gr_gid
+
+
+def file_is_persistent(path):
+ import re
+ location = r'^(/config|/opt/vyatta/etc/config)'
+ absolute = os.path.abspath(os.path.dirname(path))
+ return re.match(location,absolute)
+
+
+def commit_in_progress():
+ """ Not to be used in normal op mode scripts! """
+
+ # The CStore backend locks the config by opening a file
+ # The file is not removed after commit, so just checking
+ # if it exists is insufficient, we need to know if it's open by anyone
+
+ # There are two ways to check if any other process keeps a file open.
+ # The first one is to try opening it and see if the OS objects.
+ # That's faster but prone to race conditions and can be intrusive.
+ # The other one is to actually check if any process keeps it open.
+ # It's non-intrusive but needs root permissions, else you can't check
+ # processes of other users.
+ #
+ # Since this will be used in scripts that modify the config outside of the CLI
+ # framework, those knowingly have root permissions.
+ # For everything else, we add a safeguard.
+ from psutil import process_iter, NoSuchProcess
+ from vyos.defaults import commit_lock
+
+ idu = cmd('/usr/bin/id -u')
+ if idu != '0':
+ raise OSError("This functions needs root permissions to return correct results")
+
+ for proc in process_iter():
+ try:
+ files = proc.open_files()
+ if files:
+ for f in files:
+ if f.path == commit_lock:
+ return True
+ except NoSuchProcess as err:
+ # Process died before we could examine it
+ pass
+ # Default case
+ return False
+
+
+def wait_for_commit_lock():
+ """ Not to be used in normal op mode scripts! """
+ from time import sleep
+ # Very synchronous approach to multiprocessing
+ while commit_in_progress():
+ sleep(1)
+
+
+def ask_yes_no(question, default=False) -> bool:
+ """Ask a yes/no question via input() and return their answer."""
+ from sys import stdout
+ default_msg = "[Y/n]" if default else "[y/N]"
+ while True:
+ stdout.write("%s %s " % (question, default_msg))
+ c = input().lower()
+ if c == '':
+ return default
+ elif c in ("y", "ye", "yes"):
+ return True
+ elif c in ("n", "no"):
+ return False
+ else:
+ stdout.write("Please respond with yes/y or no/n\n")
+
+
+def is_admin() -> bool:
+ """Look if current user is in sudo group"""
+ from getpass import getuser
+ from grp import getgrnam
+ current_user = getuser()
+ (_, _, _, admin_group_members) = getgrnam('sudo')
+ return current_user in admin_group_members
+
+
+def mac2eui64(mac, prefix=None):
+ """
+ Convert a MAC address to a EUI64 address or, with prefix provided, a full
+ IPv6 address.
+ Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3
+ """
+ import re
+ from ipaddress import ip_network
+ # http://tools.ietf.org/html/rfc4291#section-2.5.1
+ eui64 = re.sub(r'[.:-]', '', mac).lower()
+ eui64 = eui64[0:6] + 'fffe' + eui64[6:]
+ eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:]
+
+ if prefix is None:
+ return ':'.join(re.findall(r'.{4}', eui64))
+ else:
+ try:
+ net = ip_network(prefix, strict=False)
+ euil = int('0x{0}'.format(eui64), 16)
+ return str(net[euil])
+ except: # pylint: disable=bare-except
+ return
+
+def get_half_cpus():
+ """ return 1/2 of the numbers of available CPUs """
+ cpu = os.cpu_count()
+ if cpu > 1:
+ cpu /= 2
+ return int(cpu)
+
+def ifname_from_config(conf):
+ """
+ Gets interface name with VLANs from current config level.
+ Level must be at the interface whose name we want.
+
+ Example:
+ >>> from vyos.util import ifname_from_config
+ >>> from vyos.config import Config
+ >>> conf = Config()
+ >>> conf.set_level('interfaces ethernet eth0 vif-s 1 vif-c 2')
+ >>> ifname_from_config(conf)
+ 'eth0.1.2'
+ """
+ level = conf.get_level()
+
+ # vlans
+ if level[-2] == 'vif' or level[-2] == 'vif-s':
+ return level[-3] + '.' + level[-1]
+ if level[-2] == 'vif-c':
+ return level[-5] + '.' + level[-3] + '.' + level[-1]
+
+ # no vlans
+ return level[-1]
+
+def get_bridge_member_config(conf, br, intf):
+ """
+ Gets bridge port (member) configuration
+
+ Arguments:
+ conf: Config
+ br: bridge name
+ intf: interface name
+
+ Returns:
+ dict with the configuration
+ False if bridge or bridge port doesn't exist
+ """
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ bridge = f'interfaces bridge {br}'
+ member = f'{bridge} member interface {intf}'
+ if not ( conf.exists(bridge) and conf.exists(member) ):
+ return False
+
+ # default bridge port configuration
+ # cost and priority initialized with linux defaults
+ # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
+ # after adding interface to bridge after reboot
+ memberconf = {
+ 'cost': 100,
+ 'priority': 32,
+ 'arp_cache_tmo': 30,
+ 'disable_link_detect': 1,
+ }
+
+ if conf.exists(f'{member} cost'):
+ memberconf['cost'] = int(conf.return_value(f'{member} cost'))
+
+ if conf.exists(f'{member} priority'):
+ memberconf['priority'] = int(conf.return_value(f'{member} priority'))
+
+ if conf.exists(f'{bridge} ip arp-cache-timeout'):
+ memberconf['arp_cache_tmo'] = int(conf.return_value(f'{bridge} ip arp-cache-timeout'))
+
+ if conf.exists(f'{bridge} disable-link-detect'):
+ memberconf['disable_link_detect'] = 2
+
+ 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')
+
+def find_device_file(device):
+ """ Recurively search /dev for the given device file and return its full path.
+ If no device file was found 'None' is returned """
+ from fnmatch import fnmatch
+
+ for root, dirs, files in os.walk('/dev'):
+ for basename in files:
+ if fnmatch(basename, device):
+ return os.path.join(root, basename)
+
+ return None
+
+def vyos_dict_search(path, dict):
+ """ Traverse Python dictionary (dict) delimited by dot (.).
+ Return value of key if found, None otherwise.
+
+ This is faster implementation then jmespath.search('foo.bar', dict)"""
+ parts = path.split('.')
+ inside = parts[:-1]
+ if not inside:
+ return dict[path]
+ c = dict
+ for p in parts[:-1]:
+ c = c.get(p, {})
+ return c.get(parts[-1], None)
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
new file mode 100644
index 000000000..ceeb6888a
--- /dev/null
+++ b/python/vyos/validate.py
@@ -0,0 +1,331 @@
+# Copyright 2018 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 json
+import socket
+import netifaces
+import ipaddress
+
+from vyos.util import cmd
+from vyos import xml
+
+# Important note when you are adding new validation functions:
+#
+# The Control class will analyse the signature of the function in this file
+# and will build the parameters to be passed to it.
+#
+# The parameter names "ifname" and "self" will get the Interface name and class
+# parameters with default will be left unset
+# all other paramters will receive the value to check
+
+
+def is_ip(addr):
+ """
+ Check addr if it is an IPv4 or IPv6 address
+ """
+ return is_ipv4(addr) or is_ipv6(addr)
+
+def is_ipv4(addr):
+ """
+ Check addr if it is an IPv4 address/network. Returns True/False
+ """
+
+ # With the below statement we can check for IPv4 networks and host
+ # addresses at the same time
+ try:
+ if ipaddress.ip_address(addr.split(r'/')[0]).version == 4:
+ return True
+ except:
+ pass
+
+ return False
+
+def is_ipv6(addr):
+ """
+ Check addr if it is an IPv6 address/network. Returns True/False
+ """
+
+ # With the below statement we can check for IPv4 networks and host
+ # addresses at the same time
+ try:
+ if ipaddress.ip_network(addr.split(r'/')[0]).version == 6:
+ return True
+ except:
+ pass
+
+ return False
+
+def is_ipv6_link_local(addr):
+ """
+ Check addr if it is an IPv6 link-local address/network. Returns True/False
+ """
+
+ addr = addr.split('%')[0]
+ if is_ipv6(addr):
+ if ipaddress.IPv6Address(addr).is_link_local:
+ return True
+
+ return False
+
+def _are_same_ip(one, two):
+ # compare the binary representation of the IP
+ f_one = socket.AF_INET if is_ipv4(one) else socket.AF_INET6
+ s_two = socket.AF_INET if is_ipv4(two) else socket.AF_INET6
+ return socket.inet_pton(f_one, one) == socket.inet_pton(f_one, two)
+
+def is_intf_addr_assigned(intf, addr):
+ if '/' in addr:
+ ip,mask = addr.split('/')
+ return _is_intf_addr_assigned(intf, ip, mask)
+ return _is_intf_addr_assigned(intf, addr)
+
+def _is_intf_addr_assigned(intf, address, netmask=''):
+ """
+ Verify if the given IPv4/IPv6 address is assigned to specific interface.
+ It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR
+ address 192.0.2.1/24.
+ """
+
+ # check if the requested address type is configured at all
+ # {
+ # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
+ # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}],
+ # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}]
+ # }
+ try:
+ ifaces = netifaces.ifaddresses(intf)
+ except ValueError as e:
+ print(e)
+ return False
+
+ # determine IP version (AF_INET or AF_INET6) depending on passed address
+ addr_type = netifaces.AF_INET if is_ipv4(address) else netifaces.AF_INET6
+
+ # Check every IP address on this interface for a match
+ for ip in ifaces.get(addr_type,[]):
+ # ip can have the interface name in the 'addr' field, we need to remove it
+ # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'}
+ ip_addr = ip['addr'].split('%')[0]
+
+ if not _are_same_ip(address, ip_addr):
+ continue
+
+ # we do not have a netmask to compare against, they are the same
+ if netmask == '':
+ return True
+
+ prefixlen = ''
+ if is_ipv4(ip_addr):
+ prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')])
+ else:
+ prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split(':') if _])
+
+ if str(prefixlen) == netmask:
+ return True
+
+ return False
+
+def is_addr_assigned(addr):
+ """
+ Verify if the given IPv4/IPv6 address is assigned to any interface
+ """
+
+ for intf in netifaces.interfaces():
+ tmp = is_intf_addr_assigned(intf, addr)
+ if tmp == True:
+ return True
+
+ return False
+
+def is_loopback_addr(addr):
+ """
+ Check if supplied IPv4/IPv6 address is a loopback address
+ """
+ return ipaddress.ip_address(addr).is_loopback
+
+def is_subnet_connected(subnet, primary=False):
+ """
+ Verify is the given IPv4/IPv6 subnet is connected to any interface on this
+ system.
+
+ primary check if the subnet is reachable via the primary IP address of this
+ interface, or in other words has a broadcast address configured. ISC DHCP
+ for instance will complain if it should listen on non broadcast interfaces.
+
+ Return True/False
+ """
+
+ # determine IP version (AF_INET or AF_INET6) depending on passed address
+ addr_type = netifaces.AF_INET
+ if is_ipv6(subnet):
+ addr_type = netifaces.AF_INET6
+
+ for interface in netifaces.interfaces():
+ # check if the requested address type is configured at all
+ if addr_type not in netifaces.ifaddresses(interface).keys():
+ continue
+
+ # An interface can have multiple addresses, but some software components
+ # only support the primary address :(
+ if primary:
+ ip = netifaces.ifaddresses(interface)[addr_type][0]['addr']
+ if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet):
+ return True
+ else:
+ # Check every assigned IP address if it is connected to the subnet
+ # in question
+ for ip in netifaces.ifaddresses(interface)[addr_type]:
+ # remove interface extension (e.g. %eth0) that gets thrown on the end of _some_ addrs
+ addr = ip['addr'].split('%')[0]
+ if ipaddress.ip_address(addr) in ipaddress.ip_network(subnet):
+ return True
+
+ return False
+
+
+def assert_boolean(b):
+ if int(b) not in (0, 1):
+ raise ValueError(f'Value {b} out of range')
+
+
+def assert_range(value, lower=0, count=3):
+ if int(value) not in range(lower,lower+count):
+ raise ValueError("Value out of range")
+
+
+def assert_list(s, l):
+ if s not in l:
+ o = ' or '.join([f'"{n}"' for n in l])
+ raise ValueError(f'state must be {o}, got {s}')
+
+
+def assert_number(n):
+ if not str(n).isnumeric():
+ raise ValueError(f'{n} must be a number')
+
+
+def assert_positive(n, smaller=0):
+ assert_number(n)
+ if int(n) < smaller:
+ raise ValueError(f'{n} is smaller than {smaller}')
+
+
+def assert_mtu(mtu, ifname):
+ assert_number(mtu)
+
+ out = cmd(f'ip -j -d link show dev {ifname}')
+ # [{"ifindex":2,"ifname":"eth0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","linkmode":"DEFAULT","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:d9:5b:04","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":46,"max_mtu":16110,"inet6_addr_gen_mode":"none","num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535}]
+ parsed = json.loads(out)[0]
+ min_mtu = int(parsed.get('min_mtu', '0'))
+ # cur_mtu = parsed.get('mtu',0),
+ max_mtu = int(parsed.get('max_mtu', '0'))
+ cur_mtu = int(mtu)
+
+ if (min_mtu and cur_mtu < min_mtu) or cur_mtu < 68:
+ raise ValueError(f'MTU is too small for interface "{ifname}": {mtu} < {min_mtu}')
+ if (max_mtu and cur_mtu > max_mtu) or cur_mtu > 65536:
+ raise ValueError(f'MTU is too small for interface "{ifname}": {mtu} > {max_mtu}')
+
+
+def assert_mac(m):
+ split = m.split(':')
+ size = len(split)
+
+ # a mac address consits out of 6 octets
+ if size != 6:
+ raise ValueError(f'wrong number of MAC octets ({size}): {m}')
+
+ octets = []
+ try:
+ for octet in split:
+ octets.append(int(octet, 16))
+ except ValueError:
+ raise ValueError(f'invalid hex number "{octet}" in : {m}')
+
+ # validate against the first mac address byte if it's a multicast
+ # address
+ if octets[0] & 1:
+ raise ValueError(f'{m} is a multicast MAC address')
+
+ # overall mac address is not allowed to be 00:00:00:00:00:00
+ if sum(octets) == 0:
+ raise ValueError('00:00:00:00:00:00 is not a valid MAC address')
+
+ if octets[:5] == (0, 0, 94, 0, 1):
+ raise ValueError(f'{m} is a VRRP MAC address')
+
+
+def is_member(conf, interface, intftype=None):
+ """
+ Checks if passed interface is member of other interface of specified type.
+ intftype is optional, if not passed it will search all known types
+ (currently bridge and bonding)
+
+ Returns:
+ None -> Interface is not a member
+ interface name -> Interface is a member of this interface
+ False -> interface type cannot have members
+ """
+ ret_val = None
+ if intftype not in ['bonding', 'bridge', None]:
+ raise ValueError((
+ f'unknown interface type "{intftype}" or it cannot '
+ f'have member interfaces'))
+
+ intftype = ['bonding', 'bridge'] if intftype == None else [intftype]
+
+ # set config level to root
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ for it in intftype:
+ base = ['interfaces', it]
+ for intf in conf.list_nodes(base):
+ memberintf = base + [intf, 'member', 'interface']
+ if xml.is_tag(memberintf):
+ if interface in conf.list_nodes(memberintf):
+ ret_val = intf
+ break
+ elif xml.is_leaf(memberintf):
+ if ( conf.exists(memberintf) and
+ interface in conf.return_values(memberintf) ):
+ ret_val = intf
+ break
+
+ old_level = conf.set_level(old_level)
+ return ret_val
+
+def has_address_configured(conf, intf):
+ """
+ Checks if interface has an address configured.
+ Checks the following config nodes:
+ 'address', 'ipv6 address eui64', 'ipv6 address autoconf'
+
+ Returns True if interface has address configured, False if it doesn't.
+ """
+ from vyos.ifconfig import Section
+ ret = False
+
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ intfpath = 'interfaces ' + Section.get_config_path(intf)
+ if ( conf.exists(f'{intfpath} address') or
+ conf.exists(f'{intfpath} ipv6 address autoconf') or
+ conf.exists(f'{intfpath} ipv6 address eui64') ):
+ ret = True
+
+ conf.set_level(old_level)
+ return ret
diff --git a/python/vyos/version.py b/python/vyos/version.py
new file mode 100644
index 000000000..871bb0f1b
--- /dev/null
+++ b/python/vyos/version.py
@@ -0,0 +1,107 @@
+# Copyright 2017-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/>.
+
+"""
+VyOS version data access library.
+
+VyOS stores its version data, which include the version number and some
+additional information in a JSON file. This module provides a convenient
+interface to reading it.
+
+Example of the version data dict::
+ {
+ 'built_by': 'autobuild@vyos.net',
+ 'build_id': '021ac2ee-cd07-448b-9991-9c68d878cddd',
+ 'version': '1.2.0-rolling+201806200337',
+ 'built_on': 'Wed 20 Jun 2018 03:37 UTC'
+ }
+"""
+
+import os
+import json
+import vyos.defaults
+
+from vyos.util import read_file
+from vyos.util import read_json
+from vyos.util import popen
+from vyos.util import run
+from vyos.util import DEVNULL
+
+
+version_file = os.path.join(vyos.defaults.directories['data'], 'version.json')
+
+
+def get_version_data(fname=version_file):
+ """
+ Get complete version data
+
+ Args:
+ file (str): path to the version file
+
+ Returns:
+ dict: version data, if it can not be found and empty dict
+
+ The optional ``file`` argument comes in handy in upgrade scripts
+ that need to retrieve information from images other than the running image.
+ It should not be used on a running system since the location of that file
+ is an implementation detail and may change in the future, while the interface
+ of this module will stay the same.
+ """
+ return read_json(fname, {})
+
+
+def get_version(fname=version_file):
+ """
+ Get the version number, or an empty string if it could not be determined
+ """
+ return get_version_data(fname=fname).get('version', '')
+
+
+def get_full_version_data(fname=version_file):
+ version_data = get_version_data(fname)
+
+ # Get system architecture (well, kernel architecture rather)
+ version_data['system_arch'], _ = popen('uname -m', stderr=DEVNULL)
+
+ hypervisor,code = popen('hvinfo', stderr=DEVNULL)
+ if code == 1:
+ # hvinfo returns 1 if it cannot detect any hypervisor
+ version_data['system_type'] = 'bare metal'
+ else:
+ version_data['system_type'] = f"{hypervisor} guest"
+
+ # Get boot type, it can be livecd, installed image, or, possible, a system installed
+ # via legacy "install system" mechanism
+ # In installed images, the squashfs image file is named after its image version,
+ # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot
+ # from an installed image
+ boot_via = "installed image"
+ if run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0:
+ boot_via = "livecd"
+ elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0:
+ boot_via = "legacy non-image installation"
+ version_data['boot_via'] = boot_via
+
+ # Get hardware details from DMI
+ dmi = '/sys/class/dmi/id'
+ version_data['hardware_vendor'] = read_file(dmi + '/sys_vendor', 'Unknown')
+ version_data['hardware_model'] = read_file(dmi +'/product_name','Unknown')
+
+ # These two assume script is run as root, normal users can't access those files
+ subsystem = '/sys/class/dmi/id/subsystem/id'
+ version_data['hardware_serial'] = read_file(subsystem + '/product_serial','Unknown')
+ version_data['hardware_uuid'] = read_file(subsystem + '/product_uuid', 'Unknown')
+
+ return version_data
diff --git a/python/vyos/xml/.gitignore b/python/vyos/xml/.gitignore
new file mode 100644
index 000000000..e934adfd1
--- /dev/null
+++ b/python/vyos/xml/.gitignore
@@ -0,0 +1 @@
+cache/
diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py
new file mode 100644
index 000000000..0f914fed2
--- /dev/null
+++ b/python/vyos/xml/__init__.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+from vyos.xml import definition
+from vyos.xml import load
+from vyos.xml import kw
+
+
+def load_configuration(cache=[]):
+ if cache:
+ return cache[0]
+
+ xml = definition.XML()
+
+ try:
+ from vyos.xml.cache import configuration
+ xml.update(configuration.definition)
+ cache.append(xml)
+ except Exception:
+ xml = definition.XML()
+ print('no xml configuration cache')
+ xml.update(load.xml(load.configuration_definition))
+
+ return xml
+
+
+# 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/cache/__init__.py b/python/vyos/xml/cache/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/xml/cache/__init__.py
diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py
new file mode 100644
index 000000000..098e64f7e
--- /dev/null
+++ b/python/vyos/xml/definition.py
@@ -0,0 +1,330 @@
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, 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:
+# {'dummy': {
+# '[node]': '[tagNode]',
+# 'address': { ... }
+# } }
+
+# so when we encounter a tagNode, we are really encountering
+# the tagNode data.
+
+
+class XML(dict):
+ def __init__(self):
+ self[kw.tree] = {}
+ self[kw.priorities] = {}
+ self[kw.owners] = {}
+ self[kw.default] = {}
+ self[kw.tags] = []
+
+ dict.__init__(self)
+
+ self.tree = self[kw.tree]
+ # the options which matched the last incomplete world we had
+ # or the last word in a list
+ self.options = []
+ # store all the part of the command we processed
+ self.inside = []
+ # should we check the data pass with the constraints
+ self.check = False
+ # are we still typing a word
+ self.filling = False
+ # do what have the tagNode value ?
+ self.filled = False
+ # last word seen
+ self.word = ''
+ # do we have all the data we want ?
+ self.final = False
+ # do we have too much data ?
+ self.extra = False
+ # what kind of node are we in plain vs data not
+ self.plain = True
+
+ def reset(self):
+ self.tree = self[kw.tree]
+ self.options = []
+ self.inside = []
+ self.check = False
+ self.filling = False
+ self.filled = False
+ self.word = ''
+ self.final = False
+ self.extra = False
+ self.plain = True
+
+ # from functools import lru_cache
+ # @lru_cache(maxsize=100)
+ # XXX: need to use cachetool instead - for later
+
+ def traverse(self, cmd):
+ self.reset()
+
+ # using split() intead of split(' ') eats the final ' '
+ words = cmd.split(' ')
+ passed = []
+ word = ''
+ data_node = False
+ space = False
+
+ while words:
+ word = words.pop(0)
+ space = word == ''
+ perfect = False
+ if word in self.tree:
+ passed = []
+ perfect = True
+ self.tree = self.tree[word]
+ data_node = self.tree[kw.node]
+ self.inside.append(word)
+ word = ''
+ continue
+ if word and data_node:
+ passed.append(word)
+
+ is_valueless = self.tree.get(kw.valueless, False)
+ is_leafNode = data_node == kw.leafNode
+ is_dataNode = data_node in (kw.leafNode, kw.tagNode)
+ named_options = [_ for _ in self.tree if not kw.found(_)]
+
+ if is_leafNode:
+ self.final = is_valueless or len(passed) > 0
+ self.extra = is_valueless and len(passed) > 0
+ self.check = len(passed) >= 1
+ else:
+ self.final = False
+ self.extra = False
+ self.check = len(passed) == 1 and not space
+
+ if self.final:
+ self.word = ' '.join(passed)
+ else:
+ self.word = word
+
+ if self.final:
+ self.filling = True
+ else:
+ self.filling = not perfect and bool(cmd and word != '')
+
+ self.filled = self.final or (is_dataNode and len(passed) > 0 and word == '')
+
+ if is_dataNode and len(passed) == 0:
+ self.options = []
+ 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
+
+ # self.debug()
+
+ return self.word
+
+ def speculate(self):
+ if len(self.options) == 1:
+ self.tree = self.tree[self.options[0]]
+ 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
+ # the first time we get the data with the node
+ # and the second with the pass parameters
+ xml = self[kw.tree]
+
+ words = cmd.split(' ')
+ send = True
+ last = []
+ while words:
+ word = words.pop(0)
+ if word in xml:
+ xml = xml[word]
+ send = True
+ last = []
+ continue
+ if xml[kw.node] in (kw.tagNode, kw.leafNode):
+ if kw.constraint in xml:
+ if send:
+ yield (word, xml[kw.constraint])
+ send = False
+ else:
+ last.append((word, None))
+ if len(last) >= 2:
+ yield last[0]
+
+ def summary(self):
+ yield ('enter', '[ summary ]', str(self.inside))
+
+ if kw.help not in self.tree:
+ yield ('skip', '[ summary ]', str(self.inside))
+ return
+
+ if self.filled:
+ return
+
+ yield('', '', '\nHelp:')
+
+ if kw.help in self.tree:
+ summary = self.tree[kw.help].get(kw.summary)
+ values = self.tree[kw.help].get(kw.valuehelp, [])
+ if summary:
+ yield(summary, '', '')
+ for value in values:
+ yield(value[kw.format], value[kw.description], '')
+
+ def constraint(self):
+ yield ('enter', '[ constraint ]', str(self.inside))
+
+ if kw.help in self.tree:
+ yield ('skip', '[ constraint ]', str(self.inside))
+ return
+ if kw.error not in self.tree:
+ yield ('skip', '[ constraint ]', str(self.inside))
+ return
+ if not self.word or self.filling:
+ yield ('skip', '[ constraint ]', str(self.inside))
+ return
+
+ yield('', '', '\nData Constraint:')
+
+ yield('', 'constraint', str(self.tree[kw.error]))
+
+ def listing(self):
+ yield ('enter', '[ listing ]', str(self.inside))
+
+ # only show the details when we passed the tagNode data
+ if not self.plain and not self.filled:
+ yield ('skip', '[ listing ]', str(self.inside))
+ return
+
+ yield('', '', '\nPossible completions:')
+
+ options = list(self.tree.keys())
+ options.sort()
+ for option in options:
+ if kw.found(option):
+ continue
+ if not option.startswith(self.word):
+ continue
+ inner = self.tree[option]
+ prefix = '+> ' if inner.get(kw.node, '') != kw.leafNode else ' '
+ if kw.help in inner:
+ yield (prefix + option, inner[kw.help].get(kw.summary), '')
+ else:
+ yield (prefix + option, '(no help available)', '')
+
+ def debug(self):
+ print('------')
+ print("word '%s'" % self.word)
+ print("filling " + str(self.filling))
+ print("filled " + str(self.filled))
+ print("final " + str(self.final))
+ print("extra " + str(self.extra))
+ print("plain " + str(self.plain))
+ print("options " + str(self.options))
+
+ # from functools import lru_cache
+ # @lru_cache(maxsize=100)
+ # XXX: need to use cachetool instead - for later
+
+ def defaults(self, lpath, flat):
+ d = self[kw.default]
+ for k in lpath:
+ 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 = {}
+ 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):
+ r.update(_flatten(level, index, d[k]))
+ continue
+ if self.is_multi(level, with_tag=False):
+ r[under] = [_.strip() for _ in d[k].split(',')]
+ continue
+ r[under] = d[k]
+ 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, 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()
+ while spath:
+ p = spath.pop(0)
+ 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, 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/generate.py b/python/vyos/xml/generate.py
new file mode 100755
index 000000000..dfbbadd74
--- /dev/null
+++ b/python/vyos/xml/generate.py
@@ -0,0 +1,70 @@
+
+#!/usr/bin/env python3
+
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+import sys
+import pprint
+import argparse
+
+from vyos.xml import kw
+from vyos.xml import load
+
+
+# import json
+# def save_json(fname, loaded):
+# with open(fname, 'w') as w:
+# print(f'saving {fname}')
+# w.write(json.dumps(loaded))
+
+
+def save_dict(fname, loaded):
+ with open(fname, 'w') as w:
+ print(f'saving {fname}')
+ w.write(f'# generated by {__file__}\n\n')
+ w.write('definition = ')
+ w.write(str(loaded))
+
+
+def main():
+ parser = argparse.ArgumentParser(description='generate python file from xml defintions')
+ parser.add_argument('--conf-folder', type=str, default=load.configuration_definition, help='XML interface definition folder')
+ parser.add_argument('--conf-cache', type=str, default=load.configuration_cache, help='python file with the conf mode dict')
+
+ # parser.add_argument('--op-folder', type=str, default=load.operational_definition, help='XML interface definition folder')
+ # parser.add_argument('--op-cache', type=str, default=load.operational_cache, help='python file with the conf mode dict')
+
+ parser.add_argument('--dry', action='store_true', help='dry run, print to screen')
+
+ args = parser.parse_args()
+
+ if os.path.exists(load.configuration_cache):
+ os.remove(load.configuration_cache)
+ # if os.path.exists(load.operational_cache):
+ # os.remove(load.operational_cache)
+
+ conf = load.xml(args.conf_folder)
+ # op = load.xml(args.op_folder)
+
+ if args.dry:
+ pprint.pprint(conf)
+ return
+
+ save_dict(args.conf_cache, conf)
+ # save_dict(args.op_cache, op)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/python/vyos/xml/kw.py b/python/vyos/xml/kw.py
new file mode 100644
index 000000000..64521c51a
--- /dev/null
+++ b/python/vyos/xml/kw.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+# all named used as key (keywords) in this module are defined here.
+# using variable name will allow the linter to warn on typos
+# it separates our dict syntax from the xmldict one, making it easy to change
+
+# we are redefining a python keyword "list" for ease
+
+
+def found(word):
+ """
+ is the word following the format for a keyword
+ """
+ return word and word[0] == '[' and word[-1] == ']'
+
+
+# root
+
+version = '[version]'
+tree = '[tree]'
+priorities = '[priorities]'
+owners = '[owners]'
+tags = '[tags]'
+default = '[default]'
+
+# nodes
+
+node = '[node]'
+
+plainNode = '[plainNode]'
+leafNode = '[leafNode]'
+tagNode = '[tagNode]'
+
+owner = '[owner]'
+
+valueless = '[valueless]'
+multi = '[multi]'
+hidden = '[hidden]'
+
+# properties
+
+priority = '[priority]'
+
+completion = '[completion]'
+list = '[list]'
+script = '[script]'
+path = '[path]'
+
+# help
+
+help = '[help]'
+
+summary = '[summary]'
+
+valuehelp = '[valuehelp]'
+format = 'format'
+description = 'description'
+
+# constraint
+
+constraint = '[constraint]'
+name = '[name]'
+
+regex = '[regex]'
+validator = '[validator]'
+argument = '[argument]'
+
+error = '[error]'
+
+# created
+
+node = '[node]'
diff --git a/python/vyos/xml/load.py b/python/vyos/xml/load.py
new file mode 100644
index 000000000..1f463a5b7
--- /dev/null
+++ b/python/vyos/xml/load.py
@@ -0,0 +1,290 @@
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import glob
+
+from os.path import join
+from os.path import abspath
+from os.path import dirname
+
+import xmltodict
+
+from vyos import debug
+from vyos.xml import kw
+from vyos.xml import definition
+
+
+# where the files are located
+
+_here = dirname(__file__)
+
+configuration_definition = abspath(join(_here, '..', '..' ,'..', 'interface-definitions'))
+configuration_cache = abspath(join(_here, 'cache', 'configuration.py'))
+
+operational_definition = abspath(join(_here, '..', '..' ,'..', 'op-mode-definitions'))
+operational_cache = abspath(join(_here, 'cache', 'operational.py'))
+
+
+# This code is only ran during the creation of the debian package
+# therefore we accept that failure can be fatal and not handled
+# gracefully.
+
+
+def _fatal(debug_info=''):
+ """
+ raise a RuntimeError or if in developer mode stop the code
+ """
+ if not debug.enabled('developer'):
+ raise RuntimeError(str(debug_info))
+
+ if debug_info:
+ print(debug_info)
+ breakpoint()
+
+
+def _safe_update(dict1, dict2):
+ """
+ return a dict made of two, raise if any root key would be overwritten
+ """
+ if set(dict1).intersection(dict2):
+ raise RuntimeError('overlapping configuration')
+ return {**dict1, **dict2}
+
+
+def _merge(dict1, dict2):
+ """
+ merge dict2 in to dict1 and return it
+ """
+ for k in list(dict2):
+ if k not in dict1:
+ dict1[k] = dict2[k]
+ continue
+ if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
+ dict1[k] = _merge(dict1[k], dict2[k])
+ elif isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
+ dict1[k].extend(dict2[k])
+ elif dict1[k] == dict2[k]:
+ # A definition shared between multiple files
+ if k in (kw.valueless, kw.multi, kw.hidden, kw.node, kw.summary, kw.owner, kw.priority):
+ continue
+ _fatal()
+ raise RuntimeError('parsing issue - undefined leaf?')
+ else:
+ raise RuntimeError('parsing issue - we messed up?')
+ return dict1
+
+
+def _include(fname, folder=''):
+ """
+ return the content of a file, including any file referenced with a #include
+ """
+ if not folder:
+ folder = dirname(fname)
+ content = ''
+ with open(fname, 'r') as r:
+ for line in r.readlines():
+ if '#include' in line:
+ content += _include(join(folder,line.strip()[10:-1]), folder)
+ continue
+ content += line
+ return content
+
+
+def _format_nodes(inside, conf, xml):
+ r = {}
+ while conf:
+ nodetype = ''
+ nodename = ''
+ if 'node' in conf.keys():
+ nodetype = 'node'
+ nodename = kw.plainNode
+ elif 'leafNode' in conf.keys():
+ nodetype = 'leafNode'
+ nodename = kw.leafNode
+ elif 'tagNode' in conf.keys():
+ nodetype = 'tagNode'
+ nodename = kw.tagNode
+ elif 'syntaxVersion' in conf.keys():
+ r[kw.version] = conf.pop('syntaxVersion')['@version']
+ continue
+ else:
+ _fatal(conf.keys())
+
+ nodes = conf.pop(nodetype)
+ if isinstance(nodes, list):
+ for node in nodes:
+ name = node.pop('@name')
+ into = inside + [name]
+ r[name] = _format_node(into, node, xml)
+ r[name][kw.node] = nodename
+ xml[kw.tags].append(' '.join(into))
+ else:
+ node = nodes
+ name = node.pop('@name')
+ into = inside + [name]
+ r[name] = _format_node(inside + [name], node, xml)
+ r[name][kw.node] = nodename
+ xml[kw.tags].append(' '.join(into))
+ return r
+
+
+def _set_validator(r, validator):
+ v = {}
+ while validator:
+ if '@name' in validator:
+ v[kw.name] = validator.pop('@name')
+ elif '@argument' in validator:
+ v[kw.argument] = validator.pop('@argument')
+ else:
+ _fatal(validator)
+ r[kw.constraint][kw.validator].append(v)
+
+
+def _format_node(inside, conf, xml):
+ r = {
+ kw.valueless: False,
+ kw.multi: False,
+ kw.hidden: False,
+ }
+
+ if '@owner' in conf:
+ owner = conf.pop('@owner', '')
+ r[kw.owner] = owner
+ xml[kw.owners][' '.join(inside)] = owner
+
+ while conf:
+ keys = conf.keys()
+ if 'children' in keys:
+ children = conf.pop('children')
+
+ if isinstance(conf, list):
+ for child in children:
+ r = _safe_update(r, _format_nodes(inside, child, xml))
+ else:
+ child = children
+ r = _safe_update(r, _format_nodes(inside, child, xml))
+
+ elif 'properties' in keys:
+ properties = conf.pop('properties')
+
+ while properties:
+ if 'help' in properties:
+ helpname = properties.pop('help')
+ r[kw.help] = {}
+ r[kw.help][kw.summary] = helpname
+
+ elif 'valueHelp' in properties:
+ valuehelps = properties.pop('valueHelp')
+ if kw.valuehelp in r[kw.help]:
+ _fatal(valuehelps)
+ r[kw.help][kw.valuehelp] = []
+ if isinstance(valuehelps, list):
+ for valuehelp in valuehelps:
+ r[kw.help][kw.valuehelp].append(dict(valuehelp))
+ else:
+ valuehelp = valuehelps
+ r[kw.help][kw.valuehelp].append(dict(valuehelp))
+
+ elif 'constraint' in properties:
+ constraint = properties.pop('constraint')
+ r[kw.constraint] = {}
+ while constraint:
+ if 'regex' in constraint:
+ regexes = constraint.pop('regex')
+ if kw.regex in kw.constraint:
+ _fatal(regexes)
+ r[kw.constraint][kw.regex] = []
+ if isinstance(regexes, list):
+ r[kw.constraint][kw.regex] = []
+ for regex in regexes:
+ r[kw.constraint][kw.regex].append(regex)
+ else:
+ regex = regexes
+ r[kw.constraint][kw.regex].append(regex)
+ elif 'validator' in constraint:
+ validators = constraint.pop('validator')
+ if kw.validator in r[kw.constraint]:
+ _fatal(validators)
+ r[kw.constraint][kw.validator] = []
+ if isinstance(validators, list):
+ for validator in validators:
+ _set_validator(r, validator)
+ else:
+ validator = validators
+ _set_validator(r, validator)
+ else:
+ _fatal(constraint)
+
+ elif 'constraintErrorMessage' in properties:
+ r[kw.error] = properties.pop('constraintErrorMessage')
+
+ elif 'valueless' in properties:
+ properties.pop('valueless')
+ r[kw.valueless] = True
+
+ elif 'multi' in properties:
+ properties.pop('multi')
+ r[kw.multi] = True
+
+ elif 'hidden' in properties:
+ properties.pop('hidden')
+ r[kw.hidden] = True
+
+ elif 'completionHelp' in properties:
+ completionHelp = properties.pop('completionHelp')
+ r[kw.completion] = {}
+ while completionHelp:
+ if 'list' in completionHelp:
+ r[kw.completion][kw.list] = completionHelp.pop('list')
+ elif 'script' in completionHelp:
+ r[kw.completion][kw.script] = completionHelp.pop('script')
+ elif 'path' in completionHelp:
+ r[kw.completion][kw.path] = completionHelp.pop('path')
+ else:
+ _fatal(completionHelp.keys())
+
+ elif 'priority' in properties:
+ priority = int(properties.pop('priority'))
+ r[kw.priority] = priority
+ xml[kw.priorities].setdefault(priority, []).append(' '.join(inside))
+
+ else:
+ _fatal(properties.keys())
+
+ elif 'defaultValue' in keys:
+ default = conf.pop('defaultValue')
+ x = xml[kw.default]
+ for k in inside[:-1]:
+ x = x.setdefault(k,{})
+ x[inside[-1]] = '' if default is None else default
+
+ else:
+ _fatal(conf)
+
+ return r
+
+
+def xml(folder):
+ """
+ read all the xml in the folder
+ """
+ xml = definition.XML()
+ for fname in glob.glob(f'{folder}/*.xml.in'):
+ parsed = xmltodict.parse(_include(fname))
+ formated = _format_nodes([], parsed['interfaceDefinition'], xml)
+ _merge(xml[kw.tree], formated)
+ # fix the configuration root node for completion
+ # as we moved all the name "up" the chain to use them as index.
+ xml[kw.tree][kw.node] = kw.plainNode
+ # XXX: do the others
+ return xml
diff --git a/python/vyos/xml/test_xml.py b/python/vyos/xml/test_xml.py
new file mode 100644
index 000000000..ff55151d2
--- /dev/null
+++ b/python/vyos/xml/test_xml.py
@@ -0,0 +1,279 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+import unittest
+from unittest import TestCase, mock
+
+from vyos.xml import load_configuration
+
+import sys
+
+
+class TestSearch(TestCase):
+ def setUp(self):
+ self.xml = load_configuration()
+
+ def test_(self):
+ last = self.xml.traverse("")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, [])
+ 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)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_i(self):
+ last = self.xml.traverse("i")
+ self.assertEqual(last, 'i')
+ self.assertEqual(self.xml.inside, [])
+ self.assertEqual(self.xml.options, ['interfaces'])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_interfaces(self):
+ last = self.xml.traverse("interfaces")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces'])
+ self.assertEqual(self.xml.options, ['bonding', 'bridge', 'dummy', 'ethernet', 'geneve', 'l2tpv3', 'loopback', 'macsec', 'openvpn', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan', 'wireguard', 'wireless', 'wirelessmodem'])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, '')
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_interfaces_space(self):
+ last = self.xml.traverse("interfaces ")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces'])
+ self.assertEqual(self.xml.options, ['bonding', 'bridge', 'dummy', 'ethernet', 'geneve', 'l2tpv3', 'loopback', 'macsec', 'openvpn', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan', 'wireguard', 'wireless', 'wirelessmodem'])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_interfaces_w(self):
+ last = self.xml.traverse("interfaces w")
+ self.assertEqual(last, 'w')
+ self.assertEqual(self.xml.inside, ['interfaces'])
+ self.assertEqual(self.xml.options, ['wireguard', 'wireless', 'wirelessmodem'])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, True)
+
+ def test_interfaces_ethernet(self):
+ last = self.xml.traverse("interfaces ethernet")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, '')
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_space(self):
+ last = self.xml.traverse("interfaces ethernet ")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, '')
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_e(self):
+ last = self.xml.traverse("interfaces ethernet e")
+ self.assertEqual(last, 'e')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_la(self):
+ last = self.xml.traverse("interfaces ethernet la")
+ self.assertEqual(last, 'la')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0(self):
+ last = self.xml.traverse("interfaces ethernet lan0")
+ self.assertEqual(last, 'lan0')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_space(self):
+ last = self.xml.traverse("interfaces ethernet lan0 ")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(len(self.xml.options), 19)
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_ad(self):
+ last = self.xml.traverse("interfaces ethernet lan0 ad")
+ self.assertEqual(last, 'ad')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet'])
+ self.assertEqual(self.xml.options, ['address'])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address ")
+ self.assertEqual(last, '')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, False)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, False)
+ self.assertEqual(self.xml.final, False)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, False)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_11(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1")
+ self.assertEqual(last, '1.1')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_1111_32(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32")
+ self.assertEqual(last, '1.1.1.1/32')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_1111_32_space(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 ")
+ self.assertEqual(last, '1.1.1.1/32')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_1111_32_space_text(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 text")
+ self.assertEqual(last, '1.1.1.1/32 text')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ def test_interfaces_ethernet_lan0_address_space_1111_32_space_text_space(self):
+ last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 text ")
+ self.assertEqual(last, '1.1.1.1/32 text')
+ self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address'])
+ self.assertEqual(self.xml.options, [])
+ self.assertEqual(self.xml.filling, True)
+ self.assertEqual(self.xml.word, last)
+ self.assertEqual(self.xml.check, True)
+ self.assertEqual(self.xml.final, True)
+ self.assertEqual(self.xml.extra, False)
+ self.assertEqual(self.xml.filled, True)
+ self.assertEqual(self.xml.plain, False)
+
+ # Need to add a check for a valuless leafNode \ No newline at end of file