diff --git a/python/vyos/config.py b/python/vyos/config.py index e5963c19a..5bd8fb072 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -1,467 +1,461 @@ # 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 os import re import json import subprocess import vyos.configtree -import vyos.util class VyOSError(Exception): """ Raised on config access errors, most commonly if the type of a config tree node in the system does not match the type of operation. """ pass 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): 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 # (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 - # The output of showConfig does not escape backslashes, as is expected - # by ConfigTree(). - session_config_text = vyos.util.escape_backslash(session_config_text) - running_config_text = vyos.util.escape_backslash(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) else: p = subprocess.Popen(cmd, stdout=subprocess.PIPE) out = p.stdout.read() p.wait() 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): 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): 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): """ Gets the current edit level. Returns: str: current edit level """ return(self._level) 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 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 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. """ 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): """ Args: 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: 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 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``. """ try: value = self._session_config.return_value(self._make_path(path)) except vyos.configtree.ConfigTreeError: 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``. """ try: values = self._session_config.return_values(self._make_path(path)) except vyos.configtree.ConfigTreeError: values = [] if not values: return(default) 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 """ try: nodes = self._session_config.list_nodes(self._make_path(path)) except vyos.configtree.ConfigTreeError: nodes = [] if not nodes: return(default) 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. """ return(self._running_config.exists(self._make_path(path))) 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 """ try: value = self._running_config.return_value(self._make_path(path)) except vyos.configtree.ConfigTreeError: 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 """ try: values = self._running_config.return_values(self._make_path(path)) except vyos.configtree.ConfigTreeError: values = [] if not values: return(default) 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 Raises: VyOSError: if the node is not a tag node """ try: nodes = self._running_config.list_nodes(self._make_path(path)) except vyos.configtree.ConfigTreeError: nodes = [] if not nodes: return(default) else: return(nodes) diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 77cffe90b..0274f3573 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -1,324 +1,331 @@ # 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 strip_comments(s): """ Split a config string into the config section and the trailing comments """ INITIAL = 0 IN_COMMENT = 1 i = len(s) - 1 state = INITIAL config_end = 0 # Find the first character of the comments section at the end, # if it exists while (i >= 0): c = s[i] if (state == INITIAL) and re.match(r'\s', c): # Ignore whitespace if (i != 0): i -= 1 else: config_end = 0 break elif (state == INITIAL) and not re.match(r'(\s|\/)', c): # Assume there are no (more) trailing comments, # this is an end of a node: either a brace of the last character # of a leaf node value config_end = i + 1 break elif (state == INITIAL) and (c == '/'): # A comment begins, or it's a stray slash if (s[i-1] == '*'): state = IN_COMMENT i -= 2 else: raise ValueError("Invalid syntax: stray slash at character {0}".format(i + 1)) elif (state == IN_COMMENT) and (c == '*'): # A comment ends here try: if (s[i-1] == '/'): state = INITIAL i -= 2 except: raise ValueError("Invalid syntax: malformed commend end at character {0}".format(i + 1)) elif (state == IN_COMMENT) and (c != '*'): # Ignore everything inside comments, including braces i -= 1 else: # Shouldn't happen raise ValueError("Invalid syntax at character {0}: invalid character {1}".format(i + 1, c)) return (s[0:config_end], s[config_end+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, comments_section = strip_comments(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.__comments = comments_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.__comments) 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(oldpath)) 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(oldpath)) 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/util.py b/python/vyos/util.py index 659a702fd..67a602f7a 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -1,207 +1,201 @@ # 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 getpass import grp import time import subprocess import sys import psutil import vyos.defaults def read_file(path): """ Read a file to string """ with open(path, 'r') as f: data = f.read().strip() return data 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. """ 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 process_running(pid_file): """ Checks if a process with PID in pid_file is running """ with open(pid_file, 'r') as f: pid = f.read().strip() return psutil.pid_exists(int(pid)) 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(): group_data = grp.getgrnam(vyos.defaults.cfg_group) return group_data.gr_gid def file_is_persistent(path): if not re.match(r'^(/config|/opt/vyatta/etc/config)', os.path.dirname(path)): warning = "Warning: file {0} is outside the /config directory\n".format(path) warning += "It will not be automatically migrated to a new image on system update" return (False, warning) else: return (True, None) 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. id = subprocess.check_output(['/usr/bin/id', '-u']).decode().strip() if id != '0': raise OSError("This functions needs root permissions to return correct results") for proc in psutil.process_iter(): try: files = proc.open_files() if files: for f in files: if f.path == vyos.defaults.commit_lock: return True except psutil.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! """ # Very synchronous approach to multiprocessing while commit_in_progress(): time.sleep(1) def ask_yes_no(question, default=False) -> bool: """Ask a yes/no question via input() and return their answer.""" default_msg = "[Y/n]" if default else "[y/N]" while True: sys.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: sys.stdout.write("Please respond with yes/y or no/n\n") def is_admin() -> bool: """Look if current user is in sudo group""" current_user = getpass.getuser() (_, _, _, admin_group_members) = grp.getgrnam('sudo') return current_user in admin_group_members - -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 diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index ba684b553..7322e0c50 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -1,166 +1,164 @@ #!/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 os from sys import exit from copy import deepcopy -from vyos.configdict import list_diff from vyos.config import Config from vyos.ifconfig import GeneveIf, Interface -from vyos.interfaces import get_type_of_interface from vyos import ConfigError from netifaces import interfaces default_config_data = { 'address': [], 'deleted': False, 'description': '', 'disable': False, 'intf': '', 'ip_arp_cache_tmo': 30, 'ip_proxy_arp': 0, 'mtu': 1500, 'remote': '', 'vni': '' } def get_config(): geneve = deepcopy(default_config_data) conf = Config() # determine tagNode instance try: geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") # Check if interface has been removed if not conf.exists('interfaces geneve ' + geneve['intf']): geneve['deleted'] = True return geneve # set new configuration level conf.set_level('interfaces geneve ' + geneve['intf']) # retrieve configured interface addresses if conf.exists('address'): geneve['address'] = conf.return_values('address') # retrieve interface description if conf.exists('description'): geneve['description'] = conf.return_value('description') # Disable this interface if conf.exists('disable'): geneve['disable'] = True # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): geneve['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) # Enable proxy-arp on this interface if conf.exists('ip enable-proxy-arp'): geneve['ip_proxy_arp'] = 1 # Maximum Transmission Unit (MTU) if conf.exists('mtu'): geneve['mtu'] = int(conf.return_value('mtu')) # Remote address of GENEVE tunnel if conf.exists('remote'): geneve['remote'] = conf.return_value('remote') # Virtual Network Identifier if conf.exists('vni'): geneve['vni'] = conf.return_value('vni') return geneve def verify(geneve): if geneve['deleted']: # bail out early return None if not geneve['remote']: raise ConfigError('GENEVE remote must be configured') if not geneve['vni']: raise ConfigError('GENEVE VNI must be configured') return None def generate(geneve): return None def apply(geneve): # Check if GENEVE interface already exists if geneve['intf'] in interfaces(): v = GeneveIf(geneve['intf']) # GENEVE is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. v.remove() if not geneve['deleted']: # GENEVE interface needs to be created on-block # instead of passing a ton of arguments, I just use a dict # that is managed by vyos.ifconfig conf = deepcopy(GeneveIf.get_config()) # Assign GENEVE instance configuration parameters to config dict conf['vni'] = geneve['vni'] conf['remote'] = geneve['remote'] # Finally create the new interface v = GeneveIf(geneve['intf'], config=conf) # update interface description used e.g. by SNMP v.set_alias(geneve['description']) # Maximum Transfer Unit (MTU) v.set_mtu(geneve['mtu']) # configure ARP cache timeout in milliseconds v.set_arp_cache_tmo(geneve['ip_arp_cache_tmo']) # Enable proxy-arp on this interface v.set_proxy_arp(geneve['ip_proxy_arp']) # Configure interface address(es) - no need to implicitly delete the # old addresses as they have already been removed by deleting the # interface above for addr in geneve['address']: v.add_addr(addr) # As the bond interface is always disabled first when changing # parameters we will only re-enable the interface if it is not # administratively disabled if not geneve['disable']: v.set_state('up') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 30ff1755d..c1fedc824 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -1,199 +1,197 @@ #!/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 os from sys import exit from copy import deepcopy -from vyos.configdict import list_diff from vyos.config import Config from vyos.ifconfig import VXLANIf, Interface -from vyos.interfaces import get_type_of_interface from vyos import ConfigError from netifaces import interfaces default_config_data = { 'address': [], 'deleted': False, 'description': '', 'disable': False, 'group': '', 'intf': '', 'ip_arp_cache_tmo': 30, 'ip_proxy_arp': 0, 'link': '', 'mtu': 1450, 'remote': '', 'remote_port': 8472, # The Linux implementation of VXLAN pre-dates # the IANA's selection of a standard destination port 'vni': '' } def get_config(): vxlan = deepcopy(default_config_data) conf = Config() # determine tagNode instance try: vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") # Check if interface has been removed if not conf.exists('interfaces vxlan ' + vxlan['intf']): vxlan['deleted'] = True return vxlan # set new configuration level conf.set_level('interfaces vxlan ' + vxlan['intf']) # retrieve configured interface addresses if conf.exists('address'): vxlan['address'] = conf.return_values('address') # retrieve interface description if conf.exists('description'): vxlan['description'] = conf.return_value('description') # Disable this interface if conf.exists('disable'): vxlan['disable'] = True # VXLAN multicast grou if conf.exists('group'): vxlan['group'] = conf.return_value('group') # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) # Enable proxy-arp on this interface if conf.exists('ip enable-proxy-arp'): vxlan['ip_proxy_arp'] = 1 # VXLAN underlay interface if conf.exists('link'): vxlan['link'] = conf.return_value('link') # Maximum Transmission Unit (MTU) if conf.exists('mtu'): vxlan['mtu'] = int(conf.return_value('mtu')) # Remote address of VXLAN tunnel if conf.exists('remote'): vxlan['remote'] = conf.return_value('remote') # Remote port of VXLAN tunnel if conf.exists('port'): vxlan['remote_port'] = int(conf.return_value('port')) # Virtual Network Identifier if conf.exists('vni'): vxlan['vni'] = conf.return_value('vni') return vxlan def verify(vxlan): if vxlan['deleted']: # bail out early return None if vxlan['mtu'] < 1500: print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') if vxlan['group'] and not vxlan['link']: raise ConfigError('Multicast VXLAN requires an underlaying interface ') if not (vxlan['group'] or vxlan['remote']): raise ConfigError('Group or remote must be configured') if not vxlan['vni']: raise ConfigError('Must configure VNI for VXLAN') if vxlan['link']: # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU # if our configured MTU is at least 50 bytes less underlay_mtu = int(Interface(vxlan['link']).get_mtu()) if underlay_mtu < (vxlan['mtu'] + 50): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ 'MTU is to small ({})'.format(underlay_mtu)) return None def generate(vxlan): return None def apply(vxlan): # Check if the VXLAN interface already exists if vxlan['intf'] in interfaces(): v = VXLANIf(vxlan['intf']) # VXLAN is super picky and the tunnel always needs to be recreated, # thus we can simply always delete it first. v.remove() if not vxlan['deleted']: # VXLAN interface needs to be created on-block # instead of passing a ton of arguments, I just use a dict # that is managed by vyos.ifconfig conf = deepcopy(VXLANIf.get_config()) # Assign VXLAN instance configuration parameters to config dict conf['vni'] = vxlan['vni'] conf['group'] = vxlan['group'] conf['dev'] = vxlan['link'] conf['remote'] = vxlan['remote'] conf['port'] = vxlan['remote_port'] # Finally create the new interface v = VXLANIf(vxlan['intf'], config=conf) # update interface description used e.g. by SNMP v.set_alias(vxlan['description']) # Maximum Transfer Unit (MTU) v.set_mtu(vxlan['mtu']) # configure ARP cache timeout in milliseconds v.set_arp_cache_tmo(vxlan['ip_arp_cache_tmo']) # Enable proxy-arp on this interface v.set_proxy_arp(vxlan['ip_proxy_arp']) # Configure interface address(es) - no need to implicitly delete the # old addresses as they have already been removed by deleting the # interface above for addr in vxlan['address']: v.add_addr(addr) # As the bond interface is always disabled first when changing # parameters we will only re-enable the interface if it is not # administratively disabled if not vxlan['disable']: v.set_state('up') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index e9a14ae98..7ae62cfb3 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -1,115 +1,112 @@ #!/usr/bin/python3 # 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 tempfile import vyos.defaults import vyos.remote import vyos.migrator from vyos.config import Config from vyos.configtree import ConfigTree if (len(sys.argv) < 2): print("Need config file name to merge.") print("Usage: merge <config file> [config path]") sys.exit(0) file_name = sys.argv[1] configdir = vyos.defaults.directories['config'] protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] if any(x in file_name for x in protocols): config_file = vyos.remote.get_remote_config(file_name) if not config_file: sys.exit("No config file by that name.") else: canonical_path = "{0}/{1}".format(configdir, file_name) first_err = None try: with open(canonical_path, 'r') as f: config_file = f.read() except Exception as err: first_err = err try: with open(file_name, 'r') as f: config_file = f.read() except Exception as err: print(first_err) print(err) sys.exit(1) with tempfile.NamedTemporaryFile() as file_to_migrate: with open(file_to_migrate.name, 'w') as fd: fd.write(config_file) migration = vyos.migrator.Migrator(file_to_migrate.name) migration.run() if migration.config_changed(): with open(file_to_migrate.name, 'r') as fd: config_file = fd.read() merge_config_tree = ConfigTree(config_file) effective_config = Config() output_effective_config = effective_config.show_config() -# showConfig (called by config.show_config() does not escape -# backslashes, which configtree expects; cf. T1001. -output_effective_config = output_effective_config.replace("\\", "\\\\") effective_config_tree = ConfigTree(output_effective_config) effective_cmds = effective_config_tree.to_commands() merge_cmds = merge_config_tree.to_commands() effective_cmd_list = effective_cmds.splitlines() merge_cmd_list = merge_cmds.splitlines() effective_cmd_set = set(effective_cmd_list) add_cmds = [ cmd for cmd in merge_cmd_list if cmd not in effective_cmd_set ] path = None if (len(sys.argv) > 2): path = sys.argv[2:] if (not effective_config_tree.exists(path) and not merge_config_tree.exists(path)): print("path {} does not exist in either effective or merge" " config; will use root.".format(path)) path = None else: path = " ".join(path) if path: add_cmds = [ cmd for cmd in add_cmds if path in cmd ] for cmd in add_cmds: cmd = "/opt/vyatta/sbin/my_" + cmd try: subprocess.check_call(cmd, shell=True) except subprocess.CalledProcessError as err: print("Called process error: {}.".format(err)) if effective_config.session_changed(): print("Merge complete. Use 'commit' to make changes effective.") else: print("No configuration changes to commit.") diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index e3644e063..46ebf5ffb 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -1,162 +1,166 @@ #!/usr/bin/env python3 # # Copyright (C) 2018 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 sys import argparse import subprocess import re from datetime import datetime, timedelta, time as type_time, date as type_date from subprocess import check_output, CalledProcessError, STDOUT from vyos.util import ask_yes_no def valid_time(s): try: return datetime.strptime(s, "%H:%M").time() except ValueError: return None def valid_date(s): try: return datetime.strptime(s, "%d%m%Y").date() except ValueError: try: return datetime.strptime(s, "%d/%m/%Y").date() except ValueError: try: return datetime.strptime(s, "%d.%m.%Y").date() except ValueError: try: return datetime.strptime(s, "%d:%m:%Y").date() except ValueError: return None def check_shutdown(): try: cmd = check_output(["/bin/systemctl","status","systemd-shutdownd.service"]) #Shutodwn is scheduled r = re.findall(r'Status: \"(.*)\"\n', cmd.decode())[0] print(r) except CalledProcessError as e: #Shutdown is not scheduled print("Shutdown is not scheduled") def cancel_shutdown(): try: - cmd = check_output(["/sbin/shutdown","-c"]) + timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + cmd = check_output(["/sbin/shutdown","-c","--no-wall"]) + message = "Reboot scheduled has been cancelled %s" % timenow + #Generate broadcast message about cancel reboot + os.system("wall %s" % message) except CalledProcessError as e: sys.exit("Error aborting shutdown: %s" % e) def execute_shutdown(time, reboot = True, ask=True): if not ask: action = "reboot" if reboot else "poweroff" if not ask_yes_no("Are you sure you want to %s this system?" % action): sys.exit(0) action = "-r" if reboot else "-P" if len(time) == 0: ### T870 legacy reboot job support chk_vyatta_based_reboots() ### cmd = check_output(["/sbin/shutdown",action,"now"],stderr=STDOUT) print(cmd.decode().split(",",1)[0]) return # Try to extract date from the first argument if len(time) == 1: time = time[0].split(" ",1) if len(time) == 1: ts = valid_time(time[0]) if time[0].isdigit() or valid_time(time[0]): cmd = check_output(["/sbin/shutdown",action,time[0]],stderr=STDOUT) else: sys.exit("Timestamp needs to be in format of 12:34") elif len(time) == 2: ts = valid_time(time[0]) ds = valid_date(time[1]) if ts and ds: t = datetime.combine(ds, ts) td = t - datetime.now() t2 = 1 + int(td.total_seconds())//60 # Get total minutes cmd = check_output(["/sbin/shutdown",action,str(t2)],stderr=STDOUT) else: sys.exit("Timestamp needs to be in format of 12:34\nDatestamp in the format of DD.MM.YY") else: sys.exit("Could not decode time and date") - print(cmd.decode().split(",",1)[0]) + check_shutdown() def chk_vyatta_based_reboots(): ### T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed ### legacy scheduled reboot s are using at and store the is as /var/run/<name>.job ### name is the node of scheduled the job, commit-confirm checks for that f = r'/var/run/confirm.job' if os .path.exists(f): jid = open(f).read().strip() if jid != 0: subprocess.call(['sudo', 'atrm', jid]) os.remove(f) def main(): parser = argparse.ArgumentParser() parser.add_argument("--yes", "-y", help="dont as for shutdown", action="store_true", dest="yes") action = parser.add_mutually_exclusive_group(required=True) action.add_argument("--reboot", "-r", help="Reboot the system", nargs="*", metavar="Minutes|HH:MM") action.add_argument("--poweroff", "-p", help="Poweroff the system", nargs="*", metavar="Minutes|HH:MM") action.add_argument("--cancel", "-c", help="Cancel pending shutdown", action="store_true") action.add_argument("--check", help="Check pending chutdown", action="store_true") args = parser.parse_args() try: if args.reboot is not None: execute_shutdown(args.reboot, reboot=True, ask=args.yes) if args.poweroff is not None: execute_shutdown(args.poweroff, reboot=False,ask=args.yes) if args.cancel: cancel_shutdown() if args.check: check_shutdown() except KeyboardInterrupt: sys.exit("Interrupted") if __name__ == "__main__": main() diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 9b6d7e979..1abaed873 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -1,337 +1,335 @@ #!/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 os import sys import grp import json import traceback import threading import vyos.config -import vyos.util 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() # Giant lock! lock = threading.Lock() def load_server_config(): with open(DEFAULT_CONFIG_FILE) as f: config = json.load(f) return config def check_auth(key_list, key): id = None for k in key_list: if k['key'] == key: id = k['id'] return id def error(code, msg): bottle.response.status = code resp = {"success": False, "error": msg, "data": None} return json.dumps(resp) 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'] strict_field = bottle.request.forms.get("strict") if strict_field == "true": strict = True else: strict = False commands = bottle.request.forms.get("data") if not commands: return error(400, "Non-empty data field is required") else: try: commands = json.loads(commands) except Exception as e: return error(400, "Failed to parse JSON: {0}".format(e)) # Allow users to pass just one command if not isinstance(commands, list): commands = [commands] # We don't want multiple people/apps to be able to commit at once, # or modify the shared session while someone else is doing the same, # so the lock is really global lock.acquire() status = 200 error_msg = None try: for c in commands: # What we've got may not even be a dict if not isinstance(c, dict): raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c))) # Missing op or path is a show stopper if not ('op' in c): raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c))) if not ('path' in c): raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c))) # Missing value is fine, substitute for empty string if 'value' in c: value = c['value'] else: value = "" op = c['op'] path = c['path'] if not path: raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c))) # Type checking if not isinstance(path, list): raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c))) if not isinstance(value, str): raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c))) # Account for the case when value field is present and set to null if not value: value = "" # For vyos.configsessios calls that have no separate value arguments, # and for type checking too try: cfg_path = " ".join(path + [value]).strip() except TypeError: raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c))) if op == 'set': # XXX: it would be nice to do a strict check for "path already exists", # but there's probably no way to do that session.set(path, value=value) elif op == 'delete': if strict and not config.exists(cfg_path): raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path)) session.delete(path, value=value) elif op == 'comment': session.comment(path, value=value) else: raise ConfigSessionError("\"{0}\" is not a valid operation".format(op)) # end for session.commit() print("Configuration modified via HTTP API using key \"{0}\"".format(id)) except ConfigSessionError as e: session.discard() status = 400 if app.config['vyos_debug']: print(traceback.format_exc(), file=sys.stderr) error_msg = str(e) except Exception as e: session.discard() print(traceback.format_exc(), file=sys.stderr) status = 500 # Don't give the details away to the outer world error_msg = "An internal error occured. Check the logs for details." finally: lock.release() if status != 200: return error(status, error_msg) else: return success(None) @app.route('/retrieve', method='POST') @auth_required def get_value(): config = app.config['vyos_config'] session = app.config['vyos_session'] command = bottle.request.forms.get("data") command = json.loads(command) try: op = command['op'] path = " ".join(command['path']) except KeyError: return error(400, "Missing required field. \"op\" and \"path\" fields are required") try: if op == 'returnValue': res = config.return_value(path) elif op == 'returnValues': res = config.return_values(path) elif op == 'exists': res = config.exists(path) elif op == 'showConfig': config_format = 'json' if 'configFormat' in command: config_format = command['configFormat'] res = session.show_config(path=command['path']) - res = vyos.util.escape_backslash(res) 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".format(config_format)) 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('/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 cfg_group = grp.getgrnam(CFG_GROUP) os.setgid(cfg_group.gr_gid) # Need to set file permissions to 775 too so that every vyattacfg group member # has write access to the running config os.umask(0o002) try: server_config = load_server_config() except Exception as e: print("Failed to load the HTTP API server config: {0}".format(e)) session = ConfigSession(os.getpid()) env = session.get_session_env() config = vyos.config.Config(session_env=env) app.config['vyos_session'] = session app.config['vyos_config'] = config app.config['vyos_keys'] = server_config['api_keys'] app.config['vyos_debug'] = server_config['debug'] bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True) diff --git a/src/utils/vyos-config-to-commands b/src/utils/vyos-config-to-commands index 7147bc5ff..8b50f7c5d 100755 --- a/src/utils/vyos-config-to-commands +++ b/src/utils/vyos-config-to-commands @@ -1,37 +1,29 @@ #!/usr/bin/python3 import sys 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) commands = config.to_commands() except ValueError as e: print("Could not parse the config file: {0}".format(e), file=sys.stderr) sys.exit(1) print(commands)