summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--interface-definitions/snmp.xml52
-rw-r--r--op-mode-definitions/wireguard.xml2
-rw-r--r--python/vyos/config.py254
-rw-r--r--python/vyos/configdict.py6
-rw-r--r--python/vyos/configsession.py18
-rw-r--r--python/vyos/configtree.py14
-rw-r--r--python/vyos/ifconfig.py98
-rw-r--r--python/vyos/iflag.py38
-rw-r--r--python/vyos/interfaces.py45
-rw-r--r--python/vyos/ioctl.py34
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py55
-rwxr-xr-xsrc/conf_mode/https.py109
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py6
-rwxr-xr-xsrc/conf_mode/snmp.py139
-rwxr-xr-xsrc/migration-scripts/snmp/0-to-156
-rwxr-xr-xsrc/op_mode/wireguard.py40
-rwxr-xr-xsrc/services/vyos-http-api-server120
-rwxr-xr-xsrc/utils/vyos-config-to-json40
18 files changed, 706 insertions, 420 deletions
diff --git a/interface-definitions/snmp.xml b/interface-definitions/snmp.xml
index c5f424e6f..bdfbd4ab6 100644
--- a/interface-definitions/snmp.xml
+++ b/interface-definitions/snmp.xml
@@ -307,15 +307,6 @@
</leafNode>
</children>
</node>
- <leafNode name="engineid">
- <properties>
- <help>Specifies the EngineID that uniquely identify an agent (e.g. 0xff42)</help>
- <constraint>
- <regex>^(0x){0,1}([0-9a-f][0-9a-f]){1,18}$</regex>
- </constraint>
- <constraintErrorMessage>ID must contain from 2 to 36 hex digits</constraintErrorMessage>
- </properties>
- </leafNode>
<leafNode name="port">
<properties>
<help>Specifies TCP/UDP port of destination SNMP traps/informs (default: '162')</help>
@@ -421,35 +412,6 @@
</leafNode>
</children>
</tagNode>
- <node name="tsm">
- <properties>
- <help>Specifies that SNMPv3 uses the Transport Security Model (TSM)</help>
- </properties>
- <children>
- <leafNode name="local-key">
- <properties>
- <help>Fingerprint of a TSM server certificate</help>
- <constraint>
- <regex>^[0-9A-F]{2}(:[0-9A-F]{2}){19}$</regex>
- </constraint>
- <constraintErrorMessage>Value can be finger print key or filename in /config/snmp/tls/certs</constraintErrorMessage>
- </properties>
- </leafNode>
- <leafNode name="port">
- <properties>
- <help>Defines the port used for TSM (default: '10161')</help>
- <valueHelp>
- <format>1-65535</format>
- <description>Numeric IP port</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-65535"/>
- </constraint>
- <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
- </properties>
- </leafNode>
- </children>
- </node>
<tagNode name="user">
<properties>
<help>Specifies the user with name username</help>
@@ -503,15 +465,6 @@
</leafNode>
</children>
</node>
- <leafNode name="engineid">
- <properties>
- <help>Specifies the EngineID that uniquely identify an agent (e.g. 0xff42)</help>
- <constraint>
- <regex>^(0x){0,1}([0-9a-f][0-9a-f]){1,18}$</regex>
- </constraint>
- <constraintErrorMessage>ID must contain from 2 to 36 hex digits</constraintErrorMessage>
- </properties>
- </leafNode>
<leafNode name="group">
<properties>
<help>Specifies group for user name</help>
@@ -582,11 +535,6 @@
</constraint>
</properties>
</leafNode>
- <leafNode name="tsm-key">
- <properties>
- <help>Specifies finger print or file name of TSM certificate</help>
- </properties>
- </leafNode>
</children>
</node>
</children>
diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml
index e52d0ad76..1795fb820 100644
--- a/op-mode-definitions/wireguard.xml
+++ b/op-mode-definitions/wireguard.xml
@@ -73,7 +73,7 @@
<script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script>
</completionHelp>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/wireguard.py "$4"</command>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard.py --showinterface "$4"</command>
<children>
<leafNode name="allowed-ips">
<properties>
diff --git a/python/vyos/config.py b/python/vyos/config.py
index c9c73b971..13b2c107e 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,22 @@ 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 json
+import subprocess
+
+import vyos.configtree
class VyOSError(Exception):
@@ -89,17 +90,49 @@ 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.
+ # In op mode, we'll just use the same running config for both active and session configs.
+ if self.in_session():
+ session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', 'showConfig'])
+ else:
+ 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 +159,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 +173,7 @@ class Config(object):
Returns:
str: current edit level
"""
- return(self._level.strip())
+ return(self._level)
def exists(self, path):
"""
@@ -148,11 +186,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):
"""
@@ -176,21 +223,33 @@ class Config(object):
except VyOSError:
return False
- def show_config(self, path='', default=None):
+ def show_config(self, path=[], default=None):
"""
Args:
- path (str): Configuration tree path, or empty
+ path (str list): Configuration tree path, or empty
default (str): Default value to return
Returns:
str: working configuration
"""
+ if isinstance(path, list):
+ path = " ".join(path)
try:
out = self._run(self._make_command('showConfig', path))
return out
except VyOSError:
return(default)
+ def get_config_dict(self, path=[], effective=False):
+ """
+ Args: path (str list): Configuration tree path, can be empty
+ Returns: a dict representation of the config
+ """
+ res = self.show_config(self._make_path(path))
+ config_tree = vyos.configtree.ConfigTree(res)
+ config_dict = json.loads(config_tree.to_json())
+ return config_dict
+
def is_multi(self, path):
"""
Args:
@@ -203,7 +262,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 +280,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 +298,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 +316,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 +323,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 +342,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 +368,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 +393,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 +405,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 +426,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 +449,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/configtree.py b/python/vyos/configtree.py
index 8832a5a63..77cffe90b 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -105,6 +105,14 @@ class ConfigTree(object):
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
@@ -184,6 +192,12 @@ class ConfigTree(object):
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'
diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py
index 3470b3aa6..df86e3c93 100644
--- a/python/vyos/ifconfig.py
+++ b/python/vyos/ifconfig.py
@@ -17,12 +17,20 @@ import os
import re
import jinja2
import json
+import glob
+import time
+import vyos.interfaces
from vyos.validate import *
+from vyos.config import Config
from ipaddress import IPv4Network, IPv6Address
from netifaces import ifaddresses, AF_INET, AF_INET6
from subprocess import Popen, PIPE, STDOUT
from time import sleep
+from os.path import isfile
+from tabulate import tabulate
+from hurry.filesize import size,alternative
+from datetime import timedelta
dhclient_base = r'/var/lib/dhcp/dhclient_'
dhcp_cfg = """
@@ -671,6 +679,31 @@ class Interface:
if os.path.isfile(self._dhcpv6_lease_file):
os.remove(self._dhcpv6_lease_file)
+ def op_show_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._ifname]
class LoopbackIf(Interface):
@@ -1371,7 +1404,6 @@ class BondIf(VLANIf):
return self._write_sysfs('/sys/class/net/{}/bonding/mode'
.format(self._ifname), mode)
-
class WireGuardIf(Interface):
"""
Wireguard interface class, contains a comnfig dictionary since
@@ -1447,6 +1479,70 @@ class WireGuardIf(Interface):
cmd = "wg set {0} peer {1} remove".format(
self._ifname, str(peerkey))
return self._cmd(cmd)
+
+ def op_show_interface(self):
+ wgdump = vyos.interfaces.wireguard_dump().get(self._ifname,None)
+
+ c = Config()
+ c.set_level(["interfaces","wireguard",self._ifname])
+ description = c.return_effective_value(["description"])
+ ips = c.return_effective_values(["address"])
+
+ print ("interface: {}".format(self._ifname))
+ 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",peer,"pubkey"])
+ 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()
+ super().op_show_interface_stats()
class VXLANIf(Interface, ):
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/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
diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py
new file mode 100644
index 000000000..e57d261e4
--- /dev/null
+++ b/python/vyos/ioctl.py
@@ -0,0 +1,34 @@
+# 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 fcntl, struct, sys
+from socket import *
+
+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(AF_INET, SOCK_DGRAM)
+ raw = fcntl.ioctl(sock.fileno(), SIOCGIFFLAGS, intf + nullif)
+ flags, = struct.unpack('H', raw[16:18])
+ return flags
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index ff3c1f825..027a7f7e3 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -13,8 +13,6 @@
#
# 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 sys
@@ -23,16 +21,17 @@ import jinja2
from vyos.config import Config
from vyos import ConfigError
-config_file = r'/etc/ddclient.conf'
+config_file = r'/etc/ddclient/ddclient.conf'
cache_file = r'/var/cache/ddclient/ddclient.cache'
+pid_file = r'/var/run/ddclient/ddclient.pid'
config_tmpl = """
### Autogenerated by dynamic_dns.py ###
daemon=1m
syslog=yes
ssl=yes
-pid=/var/run/ddclient/ddclient.pid
-cache=/var/cache/ddclient/ddclient.cache
+pid={{ pid_file }}
+cache={{ cache_file }}
{% for interface in interfaces -%}
@@ -48,11 +47,11 @@ use=if, if={{ interface.interface }}
{% for rfc in interface.rfc2136 -%}
{% for record in rfc.record %}
# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }}
-server={{ rfc.server }}
-protocol=nsupdate
-password={{ rfc.keyfile }}
-ttl={{ rfc.ttl }}
-zone={{ rfc.zone }}
+server={{ rfc.server }},
+protocol=nsupdate,
+password={{ rfc.keyfile }},
+ttl={{ rfc.ttl }},
+zone={{ rfc.zone }},
{{ record }}
{% endfor -%}
{% endfor -%}
@@ -60,12 +59,16 @@ zone={{ rfc.zone }}
{% for srv in interface.service %}
{% for host in srv.host %}
# DynDNS provider configuration for {{ host }}
-protocol={{ srv.protocol }}
-max-interval=28d
-login={{ srv.login }}
-password='{{ srv.password }}'
+protocol={{ srv.protocol }},
+max-interval=28d,
+login={{ srv.login }},
+password='{{ srv.password }}',
{% if srv.server -%}
-server={{ srv.server }}
+server={{ srv.server }},
+{% endif -%}
+{% if 'cloudflare' in srv.protocol -%}
+{% set zone = host.split('.',1) -%}
+zone={{ zone[1] }},
{% endif -%}
{{ host }}
{% endfor %}
@@ -91,6 +94,8 @@ default_service_protocol = {
default_config_data = {
'interfaces': [],
+ 'cache_file': cache_file,
+ 'pid_file': pid_file
}
def get_config():
@@ -237,8 +242,15 @@ def generate(dyndns):
if dyndns is None:
return None
- tmpl = jinja2.Template(config_tmpl)
+ dirname = os.path.dirname(dyndns['pid_file'])
+ if not os.path.exists(dirname):
+ os.mkdir(dirname)
+ dirname = os.path.dirname(config_file)
+ if not os.path.exists(dirname):
+ os.mkdir(dirname)
+
+ tmpl = jinja2.Template(config_tmpl)
config_text = tmpl.render(dyndns)
with open(config_file, 'w') as f:
f.write(config_text)
@@ -246,11 +258,16 @@ def generate(dyndns):
return None
def apply(dyndns):
- if os.path.exists(cache_file):
- os.unlink(cache_file)
+ if os.path.exists(dyndns['cache_file']):
+ os.unlink(dyndns['cache_file'])
+
+ if os.path.exists('/etc/ddclient.conf'):
+ os.unlink('/etc/ddclient.conf')
if dyndns is None:
os.system('/etc/init.d/ddclient stop')
+ if os.path.exists(dyndns['pid_file']):
+ os.unlink(dyndns['pid_file'])
else:
os.system('/etc/init.d/ddclient restart')
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index f948063e9..d7fcb74de 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -30,34 +30,34 @@ config_file = '/etc/nginx/sites-available/default'
# Please be careful if you edit the template.
config_tmpl = """
-### Autogenerated by http-api.py ###
+### Autogenerated by https.py ###
# Default server configuration
#
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
- return 302 https://$server_name$request_uri;
+ return 301 https://$server_name$request_uri;
}
-{% for addr, names in listen_addresses.items() %}
+{% for server in server_block_list %}
server {
# SSL configuration
#
-{% if addr == '*' %}
- listen 443 ssl default_server;
- listen [::]:443 ssl default_server;
+{% if server.address == '*' %}
+ listen 443 ssl;
+ listen [::]:443 ssl;
{% else %}
- listen {{ addr }}:443 ssl;
+ listen {{ server.address }}:443 ssl;
{% endif %}
-{% for name in names %}
+{% for name in server.name %}
server_name {{ name }};
{% endfor %}
-{% if vyos_cert %}
- include {{ vyos_cert.conf }};
+{% if server.vyos_cert %}
+ include {{ server.vyos_cert.conf }};
{% else %}
#
# Self signed certs generated by the ssl-cert package
@@ -67,46 +67,9 @@ server {
{% endif %}
# proxy settings for HTTP API, if enabled; 503, if not
- location ~ /(retrieve|configure) {
-{% if api %}
- proxy_pass http://localhost:{{ api.port }};
- proxy_buffering off;
-{% else %}
- return 503;
-{% endif %}
- }
-
- error_page 501 502 503 =200 @50*_json;
-
- location @50*_json {
- default_type application/json;
- return 200 '{"error": "Start service in configuration mode: set service https api"}';
- }
-
-}
-{% else %}
-server {
- # SSL configuration
- #
- listen 443 ssl default_server;
- listen [::]:443 ssl default_server;
-
- server_name _;
-
-{% if vyos_cert %}
- include {{ vyos_cert.conf }};
-{% else %}
- #
- # Self signed certs generated by the ssl-cert package
- # Don't use them in a production server!
- #
- include snippets/snakeoil.conf;
-{% endif %}
-
- # proxy settings for HTTP API, if enabled; 503, if not
- location ~ /(retrieve|configure) {
-{% if api %}
- proxy_pass http://localhost:{{ api.port }};
+ location ~ /(retrieve|configure|config-file|image) {
+{% if server.api %}
+ proxy_pass http://localhost:{{ server.api.port }};
proxy_buffering off;
{% else %}
return 503;
@@ -125,8 +88,16 @@ server {
{% endfor %}
"""
+default_server_block = {
+ 'address' : '*',
+ 'name' : ['_'],
+ # api :
+ # vyos_cert :
+ # le_cert :
+}
+
def get_config():
- https = vyos.defaults.https_data
+ server_block_list = []
conf = Config()
if not conf.exists('service https'):
return None
@@ -134,25 +105,36 @@ def get_config():
conf.set_level('service https')
if conf.exists('listen-address'):
- addrs = {}
for addr in conf.list_nodes('listen-address'):
- addrs[addr] = ['_']
+ server_block = {'address' : addr}
+ server_block['name'] = ['_']
if conf.exists('listen-address {0} server-name'.format(addr)):
names = conf.return_values('listen-address {0} server-name'.format(addr))
- addrs[addr] = names[:]
- https['listen_addresses'] = addrs
+ server_block['name'] = names[:]
+ server_block_list.append(server_block)
+ if not server_block_list:
+ server_block_list.append(default_server_block)
+
+ vyos_cert_data = {}
if conf.exists('certificates'):
if conf.exists('certificates system-generated-certificate'):
- https['vyos_cert'] = vyos.defaults.vyos_cert_data
+ vyos_cert_data = vyos.defaults.vyos_cert_data
+ if vyos_cert_data:
+ for block in server_block_list:
+ block['vyos_cert'] = vyos_cert_data
+ api_data = {}
if conf.exists('api'):
- https['api'] = vyos.defaults.api_data
-
- if conf.exists('api port'):
- port = conf.return_value('api port')
- https['api']['port'] = port
-
+ api_data = vyos.defaults.api_data
+ if conf.exists('api port'):
+ port = conf.return_value('api port')
+ api_data['port'] = port
+ if api_data:
+ for block in server_block_list:
+ block['api'] = api_data
+
+ https = {'server_block_list' : server_block_list}
return https
def verify(https):
@@ -162,6 +144,9 @@ def generate(https):
if https is None:
return None
+ if 'server_block_list' not in https or not https['server_block_list']:
+ https['server_block_list'] = [default_server_block]
+
tmpl = jinja2.Template(config_tmpl, trim_blocks=True)
config_text = tmpl.render(https)
with open(config_file, 'w') as f:
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index cd40aff3e..a9ed6bfb6 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -130,7 +130,7 @@ def get_config():
print("Interface not specified")
# check if ethernet interface has been removed
- cfg_base = 'interfaces ethernet ' + eth['intf']
+ cfg_base = ['interfaces', 'ethernet', eth['intf']]
if not conf.exists(cfg_base):
eth['deleted'] = True
# we can not bail out early as ethernet interface can not be removed
@@ -249,7 +249,7 @@ def get_config():
if conf.exists('vif-s'):
for vif_s in conf.list_nodes('vif-s'):
# set config level to vif-s interface
- conf.set_level(cfg_base + ' vif-s ' + vif_s)
+ conf.set_level(cfg_base + ['vif-s', vif_s])
eth['vif_s'].append(vlan_to_dict(conf))
# re-set configuration level to parse new nodes
@@ -263,7 +263,7 @@ def get_config():
if conf.exists('vif'):
for vif in conf.list_nodes('vif'):
# set config level to vif interface
- conf.set_level(cfg_base + ' vif ' + vif)
+ conf.set_level(cfg_base + ['vif', vif])
eth['vif'].append(vlan_to_dict(conf))
return eth
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index cba1fe319..b64cccbfa 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -13,24 +13,21 @@
#
# 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 sys
import os
-import shutil
import stat
import pwd
-import time
-
import jinja2
-import random
-import binascii
import re
import vyos.version
import vyos.validate
+from binascii import hexlify
+from shutil import move
+from time import sleep
+from stat import S_IRWXU,S_IXGRP,S_IXOTH
from vyos.config import Config
from vyos import ConfigError
@@ -78,7 +75,7 @@ createUser {{ u.name }}
{%- elif u.authPassword %}
createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }}
{%- else %}
-usmUser 1 3 {{ u.engineID }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x
+usmUser 1 3 {{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x
{%- endif %}
{%- endfor %}
@@ -117,9 +114,6 @@ monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2
########################
# configurable section #
########################
-{% if v3_tsm_key %}
-[snmp] localCert {{ v3_tsm_key }}
-{%- endif %}
# Default system description is VyOS version
sysDescr VyOS {{ version }}
@@ -130,7 +124,7 @@ SysDescr {{ description }}
{%- endif %}
# Listen
-agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161,udp6:161{% endif %}{% if v3_tsm_key %},tlstcp:{{ v3_tsm_port }},dtlsudp::{{ v3_tsm_port }}{% endif %}
+agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161,udp6:161{% endif %}
# SNMP communities
{%- for c in communities %}
@@ -192,25 +186,23 @@ view {{ v.name }} included .{{ oid.oid }}
# context sec.model sec.level match read write notif
{%- for g in v3_groups %}
access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none
-access {{ g.name }} "" tsm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none
{%- endfor %}
# trap-target
{%- for t in v3_traps %}
-trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ t.engineID }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }}
+trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }}
{%- endfor %}
# group
{%- for u in v3_users %}
group {{ u.group }} usm {{ u.name }}
-group {{ u.group }} tsm {{ u.name }}
{% endfor %}
{%- endif %}
{% if script_ext %}
# extension scripts
{%- for ext in script_ext|sort %}
-extend\t{{ext}}\t{{script_ext[ext]}}
+extend {{ ext.name }} {{ ext.script }}
{%- endfor %}
{% endif %}
"""
@@ -244,11 +236,9 @@ default_config_data = {
'v3_engineid': '',
'v3_groups': [],
'v3_traps': [],
- 'v3_tsm_key': '',
- 'v3_tsm_port': '10161',
'v3_users': [],
'v3_views': [],
- 'script_ext': {}
+ 'script_ext': []
}
def rmfile(file):
@@ -266,10 +256,10 @@ def get_config():
version_data = vyos.version.get_version_data()
snmp['version'] = version_data['version']
- # create an internal snmpv3 user of the form 'vyattaxxxxxxxxxxxxxxxx'
+ # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
# os.urandom(8) returns 8 bytes of random data
- snmp['vyos_user'] = 'vyatta' + binascii.hexlify(os.urandom(8)).decode('utf-8')
- snmp['vyos_user_pass'] = binascii.hexlify(os.urandom(16)).decode('utf-8')
+ snmp['vyos_user'] = 'vyos' + hexlify(os.urandom(8)).decode('utf-8')
+ snmp['vyos_user_pass'] = hexlify(os.urandom(16)).decode('utf-8')
if conf.exists('community'):
for name in conf.list_nodes('community'):
@@ -357,9 +347,13 @@ def get_config():
# 'set service snmp script-extensions'
#
if conf.exists('script-extensions'):
- for extname in conf.list_nodes('script-extensions extension-name'):
- snmp['script_ext'][extname] = '/config/user-data/' + conf.return_value('script-extensions extension-name ' + extname + ' script')
+ for extname in conf.list_nodes('script-extensions extension-name'):
+ extension = {
+ 'name': extname,
+ 'script' : conf.return_value('script-extensions extension-name {} script'.format(extname))
+ }
+ snmp['script_ext'].append(extension)
#########################################################################
# ____ _ _ __ __ ____ _____ #
@@ -375,15 +369,11 @@ def get_config():
else:
snmp['v3_enabled'] = True
- #
# 'set service snmp v3 engineid'
- #
if conf.exists('v3 engineid'):
snmp['v3_engineid'] = conf.return_value('v3 engineid')
- #
# 'set service snmp v3 group'
- #
if conf.exists('v3 group'):
for group in conf.list_nodes('v3 group'):
v3_group = {
@@ -404,14 +394,11 @@ def get_config():
snmp['v3_groups'].append(v3_group)
- #
# 'set service snmp v3 trap-target'
- #
if conf.exists('v3 trap-target'):
for trap in conf.list_nodes('v3 trap-target'):
trap_cfg = {
'ipAddr': trap,
- 'engineID': '',
'secName': '',
'authProtocol': 'md5',
'authPassword': '',
@@ -425,11 +412,6 @@ def get_config():
'secLevel': 'noAuthNoPriv'
}
- if conf.exists('v3 trap-target {0} engineid'.format(trap)):
- # Set the context engineID used for SNMPv3 REQUEST messages scopedPdu.
- # If not specified, this will default to the authoritative engineID.
- trap_cfg['engineID'] = conf.return_value('v3 trap-target {0} engineid'.format(trap))
-
if conf.exists('v3 trap-target {0} user'.format(trap)):
# Set the securityName used for authenticated SNMPv3 messages.
trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap))
@@ -483,19 +465,7 @@ def get_config():
snmp['v3_traps'].append(trap_cfg)
- #
- # 'set service snmp v3 tsm'
- #
- if conf.exists('v3 tsm'):
- if conf.exists('v3 tsm local-key'):
- snmp['v3_tsm_key'] = conf.return_value('v3 tsm local-key')
-
- if conf.exists('v3 tsm port'):
- snmp['v3_tsm_port'] = conf.return_value('v3 tsm port')
-
- #
# 'set service snmp v3 user'
- #
if conf.exists('v3 user'):
for user in conf.list_nodes('v3 user'):
user_cfg = {
@@ -504,19 +474,15 @@ def get_config():
'authPassword': '',
'authProtocol': 'md5',
'authOID': 'none',
- 'engineID': '',
'group': '',
'mode': 'ro',
'privMasterKey': '',
'privPassword': '',
'privOID': '',
- 'privTsmKey': '',
'privProtocol': 'des'
}
- #
# v3 user {0} auth
- #
if conf.exists('v3 user {0} auth encrypted-key'.format(user)):
user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-key'.format(user))
@@ -532,36 +498,21 @@ def get_config():
user_cfg['authProtocol'] = type
user_cfg['authOID'] = OIDs[type]
- #
- # v3 user {0} engineid
- #
- if conf.exists('v3 user {0} engineid'.format(user)):
- user_cfg['engineID'] = conf.return_value('v3 user {0} engineid'.format(user))
-
- #
# v3 user {0} group
- #
if conf.exists('v3 user {0} group'.format(user)):
user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user))
- #
# v3 user {0} mode
- #
if conf.exists('v3 user {0} mode'.format(user)):
user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user))
- #
# v3 user {0} privacy
- #
if conf.exists('v3 user {0} privacy encrypted-key'.format(user)):
user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-key'.format(user))
if conf.exists('v3 user {0} privacy plaintext-key'.format(user)):
user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-key'.format(user))
- if conf.exists('v3 user {0} privacy tsm-key'.format(user)):
- user_cfg['privTsmKey'] = conf.return_value('v3 user {0} privacy tsm-key'.format(user))
-
# load default value
type = user_cfg['privProtocol']
if conf.exists('v3 user {0} privacy type'.format(user)):
@@ -573,9 +524,7 @@ def get_config():
snmp['v3_users'].append(user_cfg)
- #
# 'set service snmp v3 view'
- #
if conf.exists('v3 view'):
for view in conf.list_nodes('v3 view'):
view_cfg = {
@@ -599,23 +548,11 @@ def verify(snmp):
### check if the configured script actually exist under /config/user-data
if snmp['script_ext']:
- for ext in snmp['script_ext']:
- if not os.path.isfile(snmp['script_ext'][ext]):
- print ("WARNING: script: " + snmp['script_ext'][ext] + " doesn\'t exist")
- else:
- os.chmod(snmp['script_ext'][ext], 0o555)
-
- # bail out early if SNMP v3 is not configured
- if not snmp['v3_enabled']:
- return None
-
- tsmKeyPattern = re.compile('^[0-9A-F]{2}(:[0-9A-F]{2}){19}$', re.IGNORECASE)
-
- if snmp['v3_tsm_key']:
- if not tsmKeyPattern.match(snmp['v3_tsm_key']):
- if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']):
- if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']):
- raise ConfigError('TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder')
+ for ext in snmp['script_ext']:
+ if not os.path.isfile(ext['script']):
+ print ("WARNING: script: {} doesn't exist".format(ext['script']))
+ else:
+ os.chmod(ext['script'], S_IRWXU|S_IXGRP|S_IXOTH)
for listen in snmp['listen_address']:
addr = listen[0]
@@ -635,6 +572,10 @@ def verify(snmp):
else:
print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+ # bail out early if SNMP v3 is not configured
+ if not snmp['v3_enabled']:
+ return None
+
if 'v3_groups' in snmp.keys():
for group in snmp['v3_groups']:
#
@@ -683,13 +624,6 @@ def verify(snmp):
if not 'privPassword' and 'privMasterKey' in trap.keys():
raise ConfigError('v3 trap: "user" must be specified')
- if 'type' in trap.keys():
- if trap['type'] == 'trap' and trap['engineID'] == '':
- raise ConfigError('must specify engineid if type is "trap"')
- else:
- raise ConfigError('"type" must be specified')
-
-
if 'v3_users' in snmp.keys():
for user in snmp['v3_users']:
#
@@ -721,21 +655,9 @@ def verify(snmp):
if user['privPassword'] == '' and user['privMasterKey'] == '':
raise ConfigError('Must specify encrypted-key or plaintext-key for user privacy')
- if user['privMasterKey'] and user['engineID'] == '':
- raise ConfigError('Can not have "encrypted-key" without engineid')
-
- if user['authPassword'] == '' and user['authMasterKey'] == '' and user['privTsmKey'] == '':
- raise ConfigError('Must specify auth or tsm-key for user auth')
-
if user['mode'] == '':
raise ConfigError('Must specify user mode ro/rw')
- if user['privTsmKey']:
- if not tsmKeyPattern.match(snmp['v3_tsm_key']):
- if not os.path.isfile('/etc/snmp/tls/certs/' + snmp['v3_tsm_key']):
- if not os.path.isfile('/config/snmp/tls/certs/' + snmp['v3_tsm_key']):
- raise ConfigError('User TSM key must be fingerprint or filename in "/config/snmp/tls/certs/" folder')
-
if 'v3_views' in snmp.keys():
for view in snmp['v3_views']:
if not view['oids']:
@@ -804,7 +726,7 @@ def apply(snmp):
if os.path.exists(volatiledir) and os.path.isdir(volatiledir):
files = os.listdir(volatiledir)
for f in files:
- shutil.move(volatiledir + '/' + f, nonvolatiledir)
+ move(volatiledir + '/' + f, nonvolatiledir)
os.chmod(nonvolatiledir + '/' + f, stat.S_IWUSR | stat.S_IRUSR)
os.rmdir(volatiledir)
@@ -825,7 +747,7 @@ def apply(snmp):
snmpReady = False
while not snmpReady:
while not os.path.exists(config_file_user):
- time.sleep(1)
+ sleep(1)
with open(config_file_user, 'r') as f:
for line in f:
@@ -858,7 +780,6 @@ def apply(snmp):
# Now update the running configuration
#
# Currently when executing os.system() the environment does not have the vyos_libexec_dir variable set, see T685
- os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set service snmp v3 user "{0}" engineid {1} > /dev/null'.format(cfg['user'], engineID))
os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key {1} > /dev/null'.format(cfg['user'], cfg['auth_pw']))
os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key {1} > /dev/null'.format(cfg['user'], cfg['priv_pw']))
os.system('vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user']))
diff --git a/src/migration-scripts/snmp/0-to-1 b/src/migration-scripts/snmp/0-to-1
new file mode 100755
index 000000000..a836f7011
--- /dev/null
+++ b/src/migration-scripts/snmp/0-to-1
@@ -0,0 +1,56 @@
+#!/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/>.
+
+import sys
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+config_base = ['service', 'snmp', 'v3']
+
+if not config.exists(config_base):
+ # Nothing to do
+ sys.exit(0)
+else:
+ # we no longer support a per trap target engine ID (https://phabricator.vyos.net/T818)
+ if config.exists(config_base + ['v3', 'trap-target']):
+ for target in config.list_nodes(config_base + ['v3', 'trap-target']):
+ config.delete(config_base + ['v3', 'trap-target', target, 'engineid'])
+
+ # we no longer support a per user engine ID (https://phabricator.vyos.net/T818)
+ if config.exists(config_base + ['v3', 'user']):
+ for user in config.list_nodes(config_base + ['v3', 'user']):
+ config.delete(config_base + ['v3', 'user', user, 'engineid'])
+
+ # we drop TSM support as there seem to be no users and this code is untested
+ # https://phabricator.vyos.net/T1769
+ if config.exists(config_base + ['v3', 'tsm']):
+ config.delete(config_base + ['v3', 'tsm'])
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
index f6978554d..38c061cf4 100755
--- a/src/op_mode/wireguard.py
+++ b/src/op_mode/wireguard.py
@@ -23,8 +23,8 @@ import shutil
import subprocess
import syslog as sl
import re
-import time
+from vyos.ifconfig import WireGuardIf
from vyos import ConfigError
from vyos.config import Config
@@ -40,41 +40,6 @@ def check_kmod():
sl.syslog(sl.LOG_ERR, "modprobe wireguard failed")
raise ConfigError("modprobe wireguard failed")
-
-def showint(interface):
- output = subprocess.check_output(["wg", "show", interface], universal_newlines=True)
- c = Config()
- c.set_level("interfaces wireguard {}".format(interface))
- description = c.return_effective_value("description".format(interface))
- """ if the interface has a description, modify the output to include it """
- if (description):
- output = re.sub(r"interface: {}".format(re.escape(interface)),"interface: {}\n Description: {}".format(interface,description),output)
-
- """ pull the last handshake times. Assume if the handshake was greater than 5 minutes, the tunnel is down """
- peer_timeouts = {}
- last_hs_output = subprocess.check_output(["wg", "show", interface, "latest-handshakes"], universal_newlines=True)
- for match in re.findall(r'(\S+)\s+(\d+)',last_hs_output):
- peer_timeouts[match[0]] = match[1]
-
- """ modify all the peers, reformat to provide VyOS config provided peername, whether the tunnel is up/down """
- for peer in c.list_effective_nodes(' peer'):
- pubkey = c.return_effective_value("peer {} pubkey".format(peer))
- status = ""
- if int(peer_timeouts[pubkey]) > 0:
- #Five minutes and the tunnel is still up
- if (time.time() - int(peer_timeouts[pubkey]) < (60*5)):
- status = "UP"
- else:
- status = "DOWN"
- elif (peer_timeouts[pubkey] is None):
- status = "DOWN"
- elif (int(peer_timeouts[pubkey]) == 0):
- status = "DOWN"
-
- output = re.sub(r"peer: {}".format(re.escape(pubkey)),"peer: {}\n Status: {}\n public key: {}".format(peer,status,pubkey),output)
-
- print(output)
-
def generate_keypair(pk, pub):
""" generates a keypair which is stored in /config/auth/wireguard """
old_umask = os.umask(0o027)
@@ -185,7 +150,8 @@ if __name__ == '__main__':
if args.listkdir:
list_key_dirs()
if args.showinterface:
- showint(args.showinterface)
+ intf = WireGuardIf(args.showinterface)
+ intf.op_show_interface()
if args.delkdir:
if args.location:
del_key_dir(args.location)
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index afab9be70..571ec1258 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -27,12 +27,13 @@ import vyos.config
import bottle
+from functools import wraps
+
from vyos.configsession import ConfigSession, ConfigSessionError
from vyos.config import VyOSError
DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
-
CFG_GROUP = 'vyattacfg'
app = bottle.default_app()
@@ -61,16 +62,23 @@ def success(data):
resp = {"success": True, "data": data, "error": None}
return json.dumps(resp)
+def auth_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ key = bottle.request.forms.get("key")
+ api_keys = app.config['vyos_keys']
+ id = check_auth(api_keys, key)
+ if not id:
+ return error(401, "Valid API key is required")
+ return f(*args, **kwargs)
+
+ return decorated_function
+
@app.route('/configure', method='POST')
+@auth_required
def configure():
session = app.config['vyos_session']
config = app.config['vyos_config']
- api_keys = app.config['vyos_keys']
-
- key = bottle.request.forms.get("key")
- id = check_auth(api_keys, key)
- if not id:
- return error(401, "Valid API key is required")
strict_field = bottle.request.forms.get("strict")
if strict_field == "true":
@@ -177,17 +185,11 @@ def configure():
return success(None)
@app.route('/retrieve', method='POST')
+@auth_required
def get_value():
config = app.config['vyos_config']
session = app.config['vyos_session']
- api_keys = app.config['vyos_keys']
-
- key = bottle.request.forms.get("key")
- id = check_auth(api_keys, key)
- if not id:
- return error(401, "Valid API key is required")
-
command = bottle.request.forms.get("data")
command = json.loads(command)
@@ -205,11 +207,21 @@ def get_value():
elif op == 'exists':
res = config.exists(path)
elif op == 'showConfig':
- config_format = 'raw'
+ config_format = 'json'
if 'configFormat' in command:
config_format = command['configFormat']
- res = session.show_config(command['path'], format=config_format)
+ res = session.show_config(path=command['path'])
+ if config_format == 'json':
+ config_tree = vyos.configtree.ConfigTree(res)
+ res = json.loads(config_tree.to_json())
+ elif config_format == 'json_ast':
+ config_tree = vyos.configtree.ConfigTree(res)
+ res = json.loads(config_tree.to_json_ast())
+ elif config_format == 'raw':
+ pass
+ else:
+ return error(400, "\"{0}\" is not a valid config format")
else:
return error(400, "\"{0}\" is not a valid operation".format(op))
except VyOSError as e:
@@ -220,6 +232,82 @@ def get_value():
return success(res)
+@app.route('/config-file', method='POST')
+@auth_required
+def config_file_op():
+ config = app.config['vyos_config']
+ session = app.config['vyos_session']
+
+ command = bottle.request.forms.get("data")
+ command = json.loads(command)
+
+ try:
+ op = command['op']
+ except KeyError:
+ return error(400, "Missing required field \"op\"")
+
+ try:
+ if op == 'save':
+ try:
+ path = command['file']
+ except KeyError:
+ path = '/config/config.boot'
+ res = session.save_config(path)
+ elif op == 'load':
+ try:
+ path = command['file']
+ except KeyError:
+ return error(400, "Missing required field \"file\"")
+ res = session.load_config(path)
+ res = session.commit()
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except VyOSError as e:
+ return error(400, str(e))
+ except Exception as e:
+ print(traceback.format_exc(), file=sys.stderr)
+ return error(500, "An internal error occured. Check the logs for details.")
+
+ return success(res)
+
+@app.route('/image', method='POST')
+@auth_required
+def config_file_op():
+ config = app.config['vyos_config']
+ session = app.config['vyos_session']
+
+ command = bottle.request.forms.get("data")
+ command = json.loads(command)
+
+ try:
+ op = command['op']
+ except KeyError:
+ return error(400, "Missing required field \"op\"")
+
+ try:
+ if op == 'add':
+ try:
+ url = command['url']
+ except KeyError:
+ return error(400, "Missing required field \"url\"")
+ res = session.install_image(url)
+ elif op == 'delete':
+ try:
+ name = command['name']
+ except KeyError:
+ return error(400, "Missing required field \"name\"")
+ res = session.remove_image(name)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except VyOSError as e:
+ return error(400, str(e))
+ except Exception as e:
+ print(traceback.format_exc(), file=sys.stderr)
+ return error(500, "An internal error occured. Check the logs for details.")
+
+ return success(res)
+
+
if __name__ == '__main__':
# systemd's user and group options don't work, do it by hand here,
# else no one else will be able to commit
diff --git a/src/utils/vyos-config-to-json b/src/utils/vyos-config-to-json
new file mode 100755
index 000000000..e03fd6a59
--- /dev/null
+++ b/src/utils/vyos-config-to-json
@@ -0,0 +1,40 @@
+#!/usr/bin/python3
+
+import sys
+import json
+
+from signal import signal, SIGPIPE, SIG_DFL
+from vyos.configtree import ConfigTree
+
+signal(SIGPIPE,SIG_DFL)
+
+config_string = None
+if (len(sys.argv) == 1):
+ # If no argument given, act as a pipe
+ config_string = sys.stdin.read()
+else:
+ file_name = sys.argv[1]
+ try:
+ with open(file_name, 'r') as f:
+ config_string = f.read()
+ except OSError as e:
+ print("Could not read config file {0}: {1}".format(file_name, e), file=sys.stderr)
+
+# This script is usually called with the output of "cli-shell-api showCfg", which does not
+# escape backslashes. "ConfigTree()" expects escaped backslashes when parsing a config
+# string (and also prints them itself). Therefore this script would fail.
+# Manually escape backslashes here to handle backslashes in any configuration strings
+# properly. The alternative would be to modify the output of "cli-shell-api showCfg",
+# but that may be break other things who rely on that specific output.
+config_string = config_string.replace("\\", "\\\\")
+
+try:
+ config = ConfigTree(config_string)
+ json_str = config.to_json()
+ # Pretty print
+ json_str = json.dumps(json.loads(json_str), indent=4, sort_keys=True)
+except ValueError as e:
+ print("Could not parse the config file: {0}".format(e), file=sys.stderr)
+ sys.exit(1)
+
+print(json_str)