summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/config.py239
-rw-r--r--python/vyos/configdict.py6
-rw-r--r--python/vyos/configsession.py18
-rw-r--r--python/vyos/interface.py141
-rw-r--r--python/vyos/interfaces.py45
5 files changed, 331 insertions, 118 deletions
diff --git a/python/vyos/config.py b/python/vyos/config.py
index c9c73b971..3a340b2da 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -1,4 +1,4 @@
-# Copyright 2017 VyOS maintainers and contributors <maintainers@vyos.io>
+# 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
@@ -17,7 +17,7 @@
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 it is safe to use
+but its API should be considered stable and safe to use
in user scripts.
Note that this module will not work outside VyOS.
@@ -43,8 +43,8 @@ For example, under "system", the names of all valid child nodes are predefined
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. The knowledge of whether in "task Foo" the "tag" is "task" or "Foo" is lost
-in time, luckily, the distinction is irrelevant in practice.
+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
###################
@@ -53,21 +53,21 @@ VyOS has two distinct modes: operational mode and configuration mode. When a use
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* config built on top of the current running config. When changes are commited, if commit succeeds,
+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.
-For this reason, this library has two sets of functions. The base versions, such as ``exists`` or ``return_value``
-are only usable in configuration mode. They take all nodes into account, in both proposed and running configs.
-Configuration scripts require access to uncommited changes for obvious reasons. Configuration mode completion helpers
-should also use these functions because not having nodes you've just created in completion is annoying.
+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.
-However, in operational mode, only the running config is available. Currently, you need to use special functions
-for reading it from operational mode scripts, they can be distinguished by the word "effective" in their names.
-In the future base versions may be made to detect if they are called from a config session or not.
"""
-import subprocess
+import os
import re
+import subprocess
+
+import vyos.configtree
class VyOSError(Exception):
@@ -89,17 +89,51 @@ class Config(object):
"""
def __init__(self, session_env=None):
self._cli_shell_api = "/bin/cli-shell-api"
- self._level = ""
+ 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
+ # (if config system is initialized at all).
+ if os.path.isfile('/tmp/vyos-config-status'):
+ running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', 'showConfig'])
+ else:
+ with open('/opt/vyatta/etc/config/config.boot') as f:
+ running_config_text = f.read()
+
+ # Session config ("active") only exists in conf mode.
+ # Trying to obtain it from op mode will cause a fatal cli-shell-api error.
+ # If that happens, we assume that a script is running from op mode and use the running config
+ # for the "session config" variable as well.
+ try:
+ session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', 'showConfig'])
+ except VyOSError:
+ session_config_text = running_config_text
+
+ self._session_config = vyos.configtree.ConfigTree(session_config_text)
+ self._running_config = vyos.configtree.ConfigTree(running_config_text)
+
def _make_command(self, op, path):
args = path.split()
cmd = [self._cli_shell_api, op] + args
return cmd
+ 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 _run(self, cmd):
if self.__session_env:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env)
@@ -126,7 +160,12 @@ class Config(object):
# 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
- self._level = path + " "
+ if isinstance(path, str):
+ self._level = re.split(r'\s*', path)
+ elif isinstance(path, list):
+ self._level = path
+ else:
+ raise TypeError("Level path must be either a whitespace-separated string or a list")
def get_level(self):
"""
@@ -135,7 +174,7 @@ class Config(object):
Returns:
str: current edit level
"""
- return(self._level.strip())
+ return(self._level)
def exists(self, path):
"""
@@ -148,11 +187,20 @@ class Config(object):
This function cannot be used outside a configuration sessions.
In operational mode scripts, use ``exists_effective``.
"""
- try:
- self._run(self._make_command('exists', self._level + path))
+ if self._session_config.exists(self._make_path(path)):
return True
- except VyOSError:
- return False
+ else:
+ # libvyosconfig exists() works only for _nodes_, not _values_
+ # libvyattacfg one also worked for values, so we emulate that case here
+ 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):
"""
@@ -203,7 +251,8 @@ class Config(object):
It also returns False if node doesn't exist.
"""
try:
- self._run(self._make_command('isMulti', self._level + path))
+ path = " ".join(self._level) + " " + path
+ self._run(self._make_command('isMulti', path))
return True
except VyOSError:
return False
@@ -220,7 +269,8 @@ class Config(object):
It also returns False if node doesn't exist.
"""
try:
- self._run(self._make_command('isTag', self._level + path))
+ path = " ".join(self._level) + " " + path
+ self._run(self._make_command('isTag', path))
return True
except VyOSError:
return False
@@ -237,7 +287,8 @@ class Config(object):
It also returns False if node doesn't exist.
"""
try:
- self._run(self._make_command('isLeaf', self._level + path))
+ path = " ".join(self._level) + " " + path
+ self._run(self._make_command('isLeaf', path))
return True
except VyOSError:
return False
@@ -254,9 +305,6 @@ class Config(object):
str: Node value, if it has any
None: if node is valueless *or* if it doesn't exist
- Raises:
- VyOSError: if node is not a single-value leaf node
-
Note:
Due to the issue with treatment of valueless nodes by this function,
valueless nodes should be checked with ``exists`` instead.
@@ -264,17 +312,15 @@ class Config(object):
This function cannot be used outside a configuration session.
In operational mode scripts, use ``return_effective_value``.
"""
- full_path = self._level + path
- if self.is_multi(path):
- raise VyOSError("Cannot use return_value on multi node: {0}".format(full_path))
- elif not self.is_leaf(path):
- raise VyOSError("Cannot use return_value on non-leaf node: {0}".format(full_path))
+ try:
+ value = self._session_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+
+ if not value:
+ return(default)
else:
- try:
- out = self._run(self._make_command('returnValue', full_path))
- return out
- except VyOSError:
- return(default)
+ return(value)
def return_values(self, path, default=[]):
"""
@@ -285,27 +331,21 @@ class Config(object):
Returns:
str list: Node values, if it has any
- None: if node does not exist
-
- Raises:
- VyOSError: if node is not a multi-value leaf node
+ []: if node does not exist
Note:
This function cannot be used outside a configuration session.
In operational mode scripts, use ``return_effective_values``.
"""
- full_path = self._level + path
- if not self.is_multi(path):
- raise VyOSError("Cannot use return_values on non-multi node: {0}".format(full_path))
- elif not self.is_leaf(path):
- raise VyOSError("Cannot use return_values on non-leaf node: {0}".format(full_path))
+ try:
+ values = self._session_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+
+ if not values:
+ return(default)
else:
- try:
- out = self._run(self._make_command('returnValues', full_path))
- values = re.findall(r"\'(.*?)\'", out)
- return values
- except VyOSError:
- return(default)
+ return(values)
def list_nodes(self, path, default=[]):
"""
@@ -317,26 +357,16 @@ class Config(object):
Returns:
string list: child node names
- Raises:
- VyOSError: if the node is not a tag node
-
- Note:
- There is no way to list all children of a non-tag node in
- the current config backend.
-
- This function cannot be used outside a configuration session.
- In operational mode scripts, use ``list_effective_nodes``.
"""
- full_path = self._level + path
- if self.is_tag(path):
- try:
- out = self._run(self._make_command('listNodes', full_path))
- values = re.findall(r"\'(.*?)\'", out)
- return values
- except VyOSError:
- return(default)
+ try:
+ nodes = self._session_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+
+ if not nodes:
+ return(default)
else:
- raise VyOSError("Cannot use list_nodes on a non-tag node: {0}".format(full_path))
+ return(nodes)
def exists_effective(self, path):
"""
@@ -352,11 +382,7 @@ class Config(object):
This function is safe to use in operational mode. In configuration mode,
it ignores uncommited changes.
"""
- try:
- self._run(self._make_command('existsEffective', self._level + path))
- return True
- except VyOSError:
- return False
+ return(self._running_config.exists(self._make_path(path)))
def return_effective_value(self, path, default=None):
"""
@@ -368,21 +394,17 @@ class Config(object):
Returns:
str: Node value
-
- Raises:
- VyOSError: if node is not a multi-value leaf node
"""
- full_path = self._level + path
- if self.is_multi(path):
- raise VyOSError("Cannot use return_effective_value on multi node: {0}".format(full_path))
- elif not self.is_leaf(path):
- raise VyOSError("Cannot use return_effective_value on non-leaf node: {0}".format(full_path))
+ try:
+ value = self._running_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+
+ if not value:
+ return(default)
else:
- try:
- out = self._run(self._make_command('returnEffectiveValue', full_path))
- return out
- except VyOSError:
- return(default)
+ return(value)
+
def return_effective_values(self, path, default=[]):
"""
@@ -393,22 +415,16 @@ class Config(object):
Returns:
str list: A list of values
-
- Raises:
- VyOSError: if node is not a multi-value leaf node
"""
- full_path = self._level + path
- if not self.is_multi(path):
- raise VyOSError("Cannot use return_effective_values on non-multi node: {0}".format(full_path))
- elif not self.is_leaf(path):
- raise VyOSError("Cannot use return_effective_values on non-leaf node: {0}".format(full_path))
+ try:
+ values = self._running_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+
+ if not values:
+ return(default)
else:
- try:
- out = self._run(self._make_command('returnEffectiveValues', full_path))
- values = re.findall(r"\'(.*?)\'", out)
- return values
- except VyOSError:
- return(default)
+ return(values)
def list_effective_nodes(self, path, default=[]):
"""
@@ -422,18 +438,13 @@ class Config(object):
Raises:
VyOSError: if the node is not a tag node
-
- Note:
- There is no way to list all children of a non-tag node in
- the current config backend.
"""
- full_path = self._level + path
- if self.is_tag(path):
- try:
- out = self._run(self._make_command('listEffectiveNodes', full_path))
- values = out.split()
- return list(map(lambda x: re.sub(r'^\'(.*)\'$', r'\1',x), values))
- except VyOSError:
- return(default)
+ try:
+ nodes = self._running_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+
+ if not nodes:
+ return(default)
else:
- raise VyOSError("Cannot use list_effective_nodes on a non-tag node: {0}".format(full_path))
+ return(nodes)
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 983906923..e8e52b33d 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -106,7 +106,7 @@ def vlan_to_dict(conf):
Function call's itself recursively if a vif-s/vif-c pair is detected.
"""
vlan = {
- 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100'
+ 'id': conf.get_level()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100'
'address': [],
'address_remove': [],
'description': '',
@@ -192,7 +192,7 @@ def vlan_to_dict(conf):
# ethertype is mandatory on vif-s nodes and only exists here!
# check if this is a vif-s node at all:
- if conf.get_level().split()[-2] == 'vif-s':
+ if conf.get_level()[-2] == 'vif-s':
vlan['vif_c'] = []
vlan['vif_c_remove'] = []
@@ -215,7 +215,7 @@ def vlan_to_dict(conf):
# add new key (vif-c) to dictionary
for vif in conf.list_nodes('vif-c'):
# set config level to vif interface
- conf.set_level(cfg_level + ' vif-c ' + vif)
+ conf.set_level(cfg_level + ['vif-c', vif])
vlan['vif_c'].append(vlan_to_dict(conf))
return vlan
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 09fae78a1..ed6288939 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -25,6 +25,9 @@ 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']
+REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
# Default "commit via" string
APP = "vyos-http-api"
@@ -36,6 +39,8 @@ APP = "vyos-http-api"
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'
@@ -160,3 +165,16 @@ class ConfigSession(object):
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
+
diff --git a/python/vyos/interface.py b/python/vyos/interface.py
new file mode 100644
index 000000000..1aae6db60
--- /dev/null
+++ b/python/vyos/interface.py
@@ -0,0 +1,141 @@
+# 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 vyos
+from vyos.config import Config
+import vyos.interfaces
+
+import re
+import json
+
+import subprocess
+import time
+from datetime import timedelta
+import glob
+from os.path import isfile
+from tabulate import tabulate
+from hurry.filesize import size,alternative
+
+class Interface():
+
+ intf = None
+ intf_type = None
+ valid = False
+
+ def __init__(self,intf):
+ self.intf = intf
+ self.intf_type = vyos.interfaces.get_type_of_interface(self.intf)
+ self.valid = (self.intf in vyos.interfaces.list_interfaces())
+
+ def print_interface(self):
+
+ if not self.valid:
+ print("Invalid interface [{}]".format(self.intf))
+ return
+
+ if (self.intf_type == 'wireguard'):
+ self.print_wireguard_interface()
+
+ self.print_interface_stats()
+
+ def print_interface_stats(self):
+ stats = self.get_interface_stats()
+ rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]]
+ tx = [['bytes','packets','errors','dropped','carrier','collisions'],[stats['tx_bytes'],stats['tx_packets'],stats['tx_errors'],stats['tx_dropped'],stats['tx_carrier_errors'],stats['collisions']]]
+ output = "RX: \n"
+ output += tabulate(rx,headers="firstrow",numalign="right",tablefmt="plain")
+ output += "\n\nTX: \n"
+ output += tabulate(tx,headers="firstrow",numalign="right",tablefmt="plain")
+ print(' '.join(('\n'+output.lstrip()).splitlines(True)))
+
+ def get_interface_stats(self):
+ interface_stats = dict()
+ devices = [f for f in glob.glob("/sys/class/net/**/statistics")]
+ for dev_path in devices:
+ metrics = [f for f in glob.glob(dev_path +"/**")]
+ dev = re.findall(r"/sys/class/net/(.*)/statistics",dev_path)[0]
+ dev_dict = dict()
+ for metric_path in metrics:
+ metric = metric_path.replace(dev_path+"/","")
+ if isfile(metric_path):
+ data = open(metric_path, 'r').read()[:-1]
+ dev_dict[metric] = int(data)
+ interface_stats[dev] = dev_dict
+
+ return interface_stats[self.intf]
+
+ def print_wireguard_interface(self):
+
+ wgdump = vyos.interfaces.wireguard_dump().get(self.intf,None)
+
+ c = Config()
+ c.set_level("interfaces wireguard {}".format(self.intf))
+ description = c.return_effective_value("description")
+ ips = c.return_effective_values("address")
+
+ print ("interface: {}".format(self.intf))
+ if (description):
+ print (" description: {}".format(description))
+
+ if (ips):
+ print (" address: {}".format(", ".join(ips)))
+ print (" public key: {}".format(wgdump['public_key']))
+ print (" private key: (hidden)")
+ print (" listening port: {}".format(wgdump['listen_port']))
+ print ()
+
+ for peer in c.list_effective_nodes(' peer'):
+ if wgdump['peers']:
+ pubkey = c.return_effective_value("peer {} pubkey".format(peer))
+ if pubkey in wgdump['peers']:
+ wgpeer = wgdump['peers'][pubkey]
+
+ print (" peer: {}".format(peer))
+ print (" public key: {}".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']))
+ print (" latest handshake: {}".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"
+ print (" status: {}".format(status))
+
+ if wgpeer['endpoint'] is not None:
+ print (" endpoint: {}".format(wgpeer['endpoint']))
+
+ if wgpeer['allowed_ips'] is not None:
+ print (" allowed ips: {}".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)
+ print (" transfer: {} received, {} sent".format(rx_size,tx_size))
+
+ if wgpeer['persistent_keepalive'] is not None:
+ print (" persistent keepalive: every {} seconds".format(wgpeer['persistent_keepalive']))
+ print()
diff --git a/python/vyos/interfaces.py b/python/vyos/interfaces.py
index d69ce9d04..ecf061d17 100644
--- a/python/vyos/interfaces.py
+++ b/python/vyos/interfaces.py
@@ -16,9 +16,9 @@
import re
import json
+import subprocess
import netifaces
-
intf_type_data_file = '/usr/share/vyos/interface-types.json'
def list_interfaces():
@@ -54,3 +54,46 @@ def get_type_of_interface(intf):
return key
raise ValueError("No type found for interface name: {0}".format(intf))
+
+def wireguard_dump():
+ """Dump wireguard data in a python friendly way."""
+ last_device=None
+ output = {}
+
+ # Dump wireguard connection data
+ _f = subprocess.check_output(["wg", "show", "all", "dump"]).decode()
+ 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