summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/config.py29
-rw-r--r--python/vyos/configsession.py148
-rw-r--r--python/vyos/defaults.py10
-rw-r--r--python/vyos/formatversions.py109
-rw-r--r--python/vyos/migrator.py192
-rw-r--r--python/vyos/remote.py136
-rw-r--r--python/vyos/systemversions.py39
-rw-r--r--python/vyos/util.py47
8 files changed, 705 insertions, 5 deletions
diff --git a/python/vyos/config.py b/python/vyos/config.py
index bcf04225b..c9c73b971 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -87,9 +87,13 @@ class Config(object):
the only state it keeps is relative *config path* for convenient access to config
subtrees.
"""
- def __init__(self):
+ 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
def _make_command(self, op, path):
args = path.split()
@@ -97,7 +101,10 @@ class Config(object):
return cmd
def _run(self, cmd):
- p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ 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:
@@ -169,6 +176,21 @@ class Config(object):
except VyOSError:
return False
+ def show_config(self, path='', default=None):
+ """
+ Args:
+ path (str): Configuration tree path, or empty
+ default (str): Default value to return
+
+ Returns:
+ str: working configuration
+ """
+ try:
+ out = self._run(self._make_command('showConfig', path))
+ return out
+ except VyOSError:
+ return(default)
+
def is_multi(self, path):
"""
Args:
@@ -383,7 +405,8 @@ class Config(object):
else:
try:
out = self._run(self._make_command('returnEffectiveValues', full_path))
- return out
+ values = re.findall(r"\'(.*?)\'", out)
+ return values
except VyOSError:
return(default)
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
new file mode 100644
index 000000000..78f332d66
--- /dev/null
+++ b/python/vyos/configsession.py
@@ -0,0 +1,148 @@
+# configsession -- the write API for the VyOS running config
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This library is free software; you can redistribute it and/or modify it under the terms of
+# the GNU Lesser General Public License as published by the Free Software Foundation;
+# either version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along with this library;
+# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+import re
+import sys
+import subprocess
+
+CLI_SHELL_API = '/bin/cli-shell-api'
+SET = '/opt/vyatta/sbin/my_set'
+DELETE = '/opt/vyatta/sbin/my_delete'
+COMMENT = '/opt/vyatta/sbin/my_comment'
+COMMIT = '/opt/vyatta/sbin/my_commit'
+DISCARD = '/opt/vyatta/sbin/my_discard'
+
+# Default "commit via" string
+APP = "vyos-http-api"
+
+# When started as a service rather than from a user shell,
+# the process lacks the VyOS-specific environment that comes
+# from bash configs, so we have to inject it
+# XXX: maybe it's better to do via a systemd environment file
+def inject_vyos_env(env):
+ env['VYATTA_CFG_GROUP_NAME'] = 'vyattacfg'
+ env['VYATTA_USER_LEVEL_DIR'] = '/opt/vyatta/etc/shell/level/admin'
+ env['vyatta_bindir']= '/opt/vyatta/bin'
+ env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
+ env['vyatta_configdir'] = '/opt/vyatta/config'
+ env['vyatta_datadir'] = '/opt/vyatta/share'
+ env['vyatta_datarootdir'] = '/opt/vyatta/share'
+ env['vyatta_libdir'] = '/opt/vyatta/lib'
+ env['vyatta_libexecdir'] = '/opt/vyatta/libexec'
+ env['vyatta_op_templates'] = '/opt/vyatta/share/vyatta-op/templates'
+ env['vyatta_prefix'] = '/opt/vyatta'
+ env['vyatta_sbindir'] = '/opt/vyatta/sbin'
+ env['vyatta_sysconfdir'] = '/opt/vyatta/etc'
+ env['vyos_bin_dir'] = '/usr/bin'
+ env['vyos_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
+ env['vyos_completion_dir'] = '/usr/libexec/vyos/completion'
+ env['vyos_configdir'] = '/opt/vyatta/config'
+ env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode'
+ env['vyos_datadir'] = '/opt/vyatta/share'
+ env['vyos_datarootdir']= '/opt/vyatta/share'
+ env['vyos_libdir'] = '/opt/vyatta/lib'
+ env['vyos_libexec_dir'] = '/usr/libexec/vyos'
+ env['vyos_op_scripts_dir'] = '/usr/libexec/vyos/op_mode'
+ env['vyos_op_templates'] = '/opt/vyatta/share/vyatta-op/templates'
+ env['vyos_prefix'] = '/opt/vyatta'
+ env['vyos_sbin_dir'] = '/usr/sbin'
+ env['vyos_validators_dir'] = '/usr/libexec/vyos/validators'
+
+ return env
+
+
+class ConfigSessionError(Exception):
+ pass
+
+
+class ConfigSession(object):
+ """
+ The write API of VyOS.
+ """
+ def __init__(self, session_id, app=APP):
+ """
+ Creates a new config session.
+
+ Args:
+ session_id (str): Session identifier
+ app (str): Application name, purely informational
+
+ Note:
+ The session identifier MUST be globally unique within the system.
+ The best practice is to only have one ConfigSession object per process
+ and used the PID for the session identifier.
+ """
+
+ env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)])
+ self.__session_id = session_id
+
+ # Extract actual variables from the chunk of shell it outputs
+ # XXX: it's better to extend cli-shell-api to provide easily readable output
+ env_list = re.findall(r'([A-Z_]+)=([^;\s]+)', env_str.decode())
+
+ session_env = os.environ
+ session_env = inject_vyos_env(session_env)
+ for k, v in env_list:
+ session_env[k] = v
+
+ self.__session_env = session_env
+ self.__session_env["COMMIT_VIA"] = app
+
+ self.__run_command([CLI_SHELL_API, 'setupSession'])
+
+ def __del__(self):
+ try:
+ output = subprocess.check_output([CLI_SHELL_API, 'teardownSession'], env=self.__session_env).decode().strip()
+ if output:
+ print("cli-shell-api teardownSession output for sesion {0}: {1}".format(self.__session_id, output), file=sys.stderr)
+ except Exception as e:
+ print("Could not tear down session {0}: {1}".format(self.__session_id, e), file=sys.stderr)
+
+ def __run_command(self, cmd_list):
+ p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env)
+ result = p.wait()
+ output = p.stdout.read().decode()
+ if result != 0:
+ raise ConfigSessionError(output)
+
+ def get_session_env(self):
+ return self.__session_env
+
+ def set(self, path, value=None):
+ if not value:
+ value = []
+ else:
+ value = [value]
+ self.__run_command([SET] + path + value)
+
+ def delete(self, path, value=None):
+ if not value:
+ value = []
+ else:
+ value = [value]
+ self.__run_command([DELETE] + path + value)
+
+ def comment(self, path, value=None):
+ if not value:
+ value = [""]
+ else:
+ value = [value]
+ self.__run_command([COMMENT] + path + value)
+
+ def commit(self):
+ self.__run_command([COMMIT])
+
+ def discard(self):
+ self.__run_command([DISCARD])
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 36185f16a..524b80424 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -15,7 +15,15 @@
directories = {
- "data": "/usr/share/vyos/"
+ "data": "/usr/share/vyos/",
+ "conf_mode": "/usr/libexec/vyos/conf_mode",
+ "config": "/opt/vyatta/etc/config",
+ "current": "/opt/vyatta/etc/config-migrate/current",
+ "migrate": "/opt/vyatta/etc/config-migrate/migrate",
}
cfg_group = 'vyattacfg'
+
+cfg_vintage = 'vyatta'
+
+commit_lock = '/opt/vyatta/config/.lock'
diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py
new file mode 100644
index 000000000..29117a5d3
--- /dev/null
+++ b/python/vyos/formatversions.py
@@ -0,0 +1,109 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import os
+import re
+import fileinput
+
+def read_vyatta_versions(config_file):
+ config_file_versions = {}
+
+ with open(config_file, 'r') as config_file_handle:
+ for config_line in config_file_handle:
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line):
+ if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line):
+ raise ValueError("malformed configuration string: "
+ "{}".format(config_line))
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
+ config_file_versions[pair[0]] = int(pair[1])
+
+
+ return config_file_versions
+
+def read_vyos_versions(config_file):
+ config_file_versions = {}
+
+ with open(config_file, 'r') as config_file_handle:
+ for config_line in config_file_handle:
+ if re.match(r'// vyos-config-version:.+', config_line):
+ if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line):
+ raise ValueError("malformed configuration string: "
+ "{}".format(config_line))
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
+ config_file_versions[pair[0]] = int(pair[1])
+
+ return config_file_versions
+
+def remove_versions(config_file):
+ """
+ Remove old version string.
+ """
+ for line in fileinput.input(config_file, inplace=True):
+ if re.match(r'/\* Warning:.+ \*/$', line):
+ continue
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
+ continue
+ if re.match(r'/\* Release version:.+ \*/$', line):
+ continue
+ if re.match('// vyos-config-version:.+', line):
+ continue
+ if re.match('// Warning:.+', line):
+ continue
+ if re.match('// Release version:.+', line):
+ continue
+ sys.stdout.write(line)
+
+def format_versions_string(config_versions):
+ cfg_keys = list(config_versions.keys())
+ cfg_keys.sort()
+
+ component_version_strings = []
+
+ for key in cfg_keys:
+ cfg_vers = config_versions[key]
+ component_version_strings.append('{}@{}'.format(key, cfg_vers))
+
+ separator = ":"
+ component_version_string = separator.join(component_version_strings)
+
+ return component_version_string
+
+def write_vyatta_versions_foot(config_file, component_version_string,
+ os_version_string):
+ if config_file:
+ with open(config_file, 'a') as config_file_handle:
+ config_file_handle.write('/* Warning: Do not remove the following line. */\n')
+ config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
+ config_file_handle.write('/* Release version: {} */\n'.format(os_version_string))
+ else:
+ sys.stdout.write('/* Warning: Do not remove the following line. */\n')
+ sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
+ sys.stdout.write('/* Release version: {} */\n'.format(os_version_string))
+
+def write_vyos_versions_foot(config_file, component_version_string,
+ os_version_string):
+ if config_file:
+ with open(config_file, 'a') as config_file_handle:
+ config_file_handle.write('// Warning: Do not remove the following line.\n')
+ config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string))
+ config_file_handle.write('// Release version: {}\n'.format(os_version_string))
+ else:
+ sys.stdout.write('// Warning: Do not remove the following line.\n')
+ sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string))
+ sys.stdout.write('// Release version: {}\n'.format(os_version_string))
+
diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
new file mode 100644
index 000000000..59d68f0f7
--- /dev/null
+++ b/python/vyos/migrator.py
@@ -0,0 +1,192 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import os
+import subprocess
+import vyos.version
+import vyos.defaults
+import vyos.systemversions as systemversions
+import vyos.formatversions as formatversions
+
+class MigratorError(Exception):
+ pass
+
+class Migrator(object):
+ def __init__(self, config_file, force=False, set_vintage=None):
+ self._config_file = config_file
+ self._force = force
+ self._set_vintage = set_vintage
+ self._config_file_vintage = None
+ self._changed = False
+
+ def read_config_file_versions(self):
+ """
+ Get component versions from config file footer and set vintage;
+ return empty dictionary if config string is missing.
+ """
+ cfg_file = self._config_file
+ component_versions = {}
+
+ cfg_versions = formatversions.read_vyatta_versions(cfg_file)
+
+ if cfg_versions:
+ self._config_file_vintage = 'vyatta'
+ component_versions = cfg_versions
+
+ cfg_versions = formatversions.read_vyos_versions(cfg_file)
+
+ if cfg_versions:
+ self._config_file_vintage = 'vyos'
+ component_versions = cfg_versions
+
+ return component_versions
+
+ def update_vintage(self):
+ old_vintage = self._config_file_vintage
+
+ if self._set_vintage:
+ self._config_file_vintage = self._set_vintage
+
+ if not self._config_file_vintage:
+ self._config_file_vintage = vyos.defaults.cfg_vintage
+
+ if self._config_file_vintage not in ['vyatta', 'vyos']:
+ raise MigratorError("Unknown vintage.")
+
+ if self._config_file_vintage == old_vintage:
+ return False
+ else:
+ return True
+
+ def run_migration_scripts(self, config_file_versions, system_versions):
+ """
+ Run migration scripts iteratively, until config file version equals
+ system component version.
+ """
+ cfg_versions = config_file_versions
+ sys_versions = system_versions
+
+ sys_keys = list(sys_versions.keys())
+ sys_keys.sort()
+
+ rev_versions = {}
+
+ for key in sys_keys:
+ sys_ver = sys_versions[key]
+ if key in cfg_versions:
+ cfg_ver = cfg_versions[key]
+ else:
+ cfg_ver = 0
+
+ migrate_script_dir = os.path.join(
+ vyos.defaults.directories['migrate'], key)
+
+ while cfg_ver < sys_ver:
+ next_ver = cfg_ver + 1
+
+ migrate_script = os.path.join(migrate_script_dir,
+ '{}-to-{}'.format(cfg_ver, next_ver))
+
+ try:
+ subprocess.check_output([migrate_script,
+ self._config_file])
+ except FileNotFoundError:
+ pass
+ except subprocess.CalledProcessError as err:
+ print("Called process error: {}.".format(err))
+ sys.exit(1)
+
+ cfg_ver = next_ver
+
+ rev_versions[key] = cfg_ver
+
+ return rev_versions
+
+ def write_config_file_versions(self, cfg_versions):
+ """
+ Write new versions string.
+ """
+ versions_string = formatversions.format_versions_string(cfg_versions)
+
+ os_version_string = vyos.version.get_version()
+
+ if self._config_file_vintage == 'vyatta':
+ formatversions.write_vyatta_versions_foot(self._config_file,
+ versions_string,
+ os_version_string)
+
+ if self._config_file_vintage == 'vyos':
+ formatversions.write_vyos_versions_foot(self._config_file,
+ versions_string,
+ os_version_string)
+
+ def run(self):
+ """
+ Gather component versions from config file and system.
+ Run migration scripts.
+ Update vintage ('vyatta' or 'vyos'), if needed.
+ If changed, remove old versions string from config file, and
+ write new versions string.
+ """
+ cfg_file = self._config_file
+
+ cfg_versions = self.read_config_file_versions()
+ if self._force:
+ # This will force calling all migration scripts:
+ cfg_versions = {}
+
+ sys_versions = systemversions.get_system_versions()
+
+ rev_versions = self.run_migration_scripts(cfg_versions, sys_versions)
+
+ if rev_versions != cfg_versions:
+ self._changed = True
+
+ if self.update_vintage():
+ self._changed = True
+
+ if not self._changed:
+ return
+
+ formatversions.remove_versions(cfg_file)
+
+ self.write_config_file_versions(rev_versions)
+
+ def config_changed(self):
+ return self._changed
+
+class VirtualMigrator(Migrator):
+ def __init__(self, config_file, vintage='vyos'):
+ super().__init__(config_file, set_vintage = vintage)
+
+ def run(self):
+ cfg_file = self._config_file
+
+ cfg_versions = self.read_config_file_versions()
+ if not cfg_versions:
+ raise MigratorError("Config file has no version information;"
+ " virtual migration not possible.")
+
+ if self.update_vintage():
+ self._changed = True
+
+ if not self._changed:
+ return
+
+ formatversions.remove_versions(cfg_file)
+
+ self.write_config_file_versions(cfg_versions)
+
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
new file mode 100644
index 000000000..49936ec08
--- /dev/null
+++ b/python/vyos/remote.py
@@ -0,0 +1,136 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+import os
+import re
+import fileinput
+import subprocess
+
+
+def check_and_add_host_key(host_name):
+ """
+ Filter host keys and prompt for adding key to known_hosts file, if
+ needed.
+ """
+ known_hosts = '{}/.ssh/known_hosts'.format(os.getenv('HOME'))
+ if not os.path.exists(known_hosts):
+ mode = 0o600
+ os.mknod(known_hosts, 0o600)
+
+ keyscan_cmd = 'ssh-keyscan -t rsa {} 2>/dev/null'.format(host_name)
+
+ try:
+ host_key = subprocess.check_output(keyscan_cmd, shell=True,
+ stderr=subprocess.DEVNULL,
+ universal_newlines=True)
+ except subprocess.CalledProcessError as err:
+ sys.exit("Can not get RSA host key")
+
+ # libssh2 (jessie; stretch) does not recognize ec host keys, and curl
+ # will fail with error 51 if present in known_hosts file; limit to rsa.
+ usable_keys = False
+ offending_keys = []
+ for line in fileinput.input(known_hosts, inplace=True):
+ if host_name in line and 'ssh-rsa' in line:
+ if line.split()[-1] != host_key.split()[-1]:
+ offending_keys.append(line)
+ continue
+ else:
+ usable_keys = True
+ if host_name in line and not 'ssh-rsa' in line:
+ continue
+
+ sys.stdout.write(line)
+
+ if usable_keys:
+ return
+
+ if offending_keys:
+ print("Host key has changed!")
+ print("If you trust the host key fingerprint below, continue.")
+
+ fingerprint_cmd = 'ssh-keygen -lf /dev/stdin <<< "{}"'.format(host_key)
+ try:
+ fingerprint = subprocess.check_output(fingerprint_cmd, shell=True,
+ stderr=subprocess.DEVNULL,
+ universal_newlines=True)
+ except subprocess.CalledProcessError as err:
+ sys.exit("Can not get RSA host key fingerprint.")
+
+ print("RSA host key fingerprint is {}".format(fingerprint.split()[1]))
+ response = input("Do you trust this host? [y]/n ")
+
+ if not response or response == 'y':
+ with open(known_hosts, 'a+') as f:
+ print("Adding {} to the list of known"
+ " hosts.".format(host_name))
+ f.write(host_key)
+ else:
+ sys.exit("Host not trusted")
+
+def get_remote_config(remote_file):
+ """ Invoke curl to download remote (config) file.
+
+ Args:
+ remote file URI:
+ scp://<user>[:<passwd>]@<host>/<file>
+ sftp://<user>[:<passwd>]@<host>/<file>
+ http://<host>/<file>
+ https://<host>/<file>
+ ftp://<user>[:<passwd>]@<host>/<file>
+ tftp://<host>/<file>
+ """
+ request = dict.fromkeys(['protocol', 'host', 'file', 'user', 'passwd'])
+ protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp']
+ or_protocols = '|'.join(protocols)
+
+ request_match = re.match(r'(' + or_protocols + r')://(.*?)(/.*)',
+ remote_file)
+ if request_match:
+ (request['protocol'], request['host'],
+ request['file']) = request_match.groups()
+ else:
+ print("Malformed URI")
+ sys.exit(1)
+
+ user_match = re.search(r'(.*)@(.*)', request['host'])
+ if user_match:
+ request['user'] = user_match.groups()[0]
+ request['host'] = user_match.groups()[1]
+ passwd_match = re.search(r'(.*):(.*)', request['user'])
+ if passwd_match:
+ # Deprectated in RFC 3986, but maintain for backward compatability.
+ request['user'] = passwd_match.groups()[0]
+ request['passwd'] = passwd_match.groups()[1]
+
+ remote_file = '{0}://{1}{2}'.format(request['protocol'], request['host'], request['file'])
+
+ if request['protocol'] in ('scp', 'sftp'):
+ check_and_add_host_key(request['host'])
+
+ if request['user'] and not request['passwd']:
+ curl_cmd = 'curl -# -u {0} {1}'.format(request['user'], remote_file)
+ else:
+ curl_cmd = 'curl -# {0}'.format(remote_file)
+
+ config_file = None
+ try:
+ config_file = subprocess.check_output(curl_cmd, shell=True,
+ universal_newlines=True)
+ except subprocess.CalledProcessError as err:
+ print("Called process error: {}.".format(err))
+
+ return config_file
diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py
new file mode 100644
index 000000000..9b3f4f413
--- /dev/null
+++ b/python/vyos/systemversions.py
@@ -0,0 +1,39 @@
+# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import sys
+import vyos.defaults
+
+def get_system_versions():
+ """
+ Get component versions from running system; critical failure if
+ unable to read migration directory.
+ """
+ system_versions = {}
+
+ try:
+ version_info = os.listdir(vyos.defaults.directories['current'])
+ except OSError as err:
+ print("OS error: {}".format(err))
+ sys.exit(1)
+
+ for info in version_info:
+ if re.match(r'[\w,-]+@\d+', info):
+ pair = info.split('@')
+ system_versions[pair[0]] = int(pair[1])
+
+ return system_versions
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 8b5342575..6ab606983 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -1,4 +1,4 @@
-# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io>
+# 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
@@ -16,6 +16,9 @@
import os
import re
import grp
+import time
+import subprocess
+
import psutil
import vyos.defaults
@@ -131,3 +134,45 @@ def file_is_persistent(path):
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)
+