summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Estabrook <jestabro@sentrium.io>2019-05-29 12:05:33 -0500
committerGitHub <noreply@github.com>2019-05-29 12:05:33 -0500
commit0a16c85a8b6aa728e142156e0e985ac21916db20 (patch)
tree2c6dabba3712f9f05662844fbd4b9994ce937a05
parentbf0f721432fa05bbc7058a0b43e2acf4ad1f30e3 (diff)
parent456abc2aa4ae10981c2aec2d2e6d975ef30fb8d6 (diff)
downloadvyos-1x-0a16c85a8b6aa728e142156e0e985ac21916db20.tar.gz
vyos-1x-0a16c85a8b6aa728e142156e0e985ac21916db20.zip
Merge pull request #68 from jestabro/merge-config
T1397: Rewrite the config merge script
-rw-r--r--python/vyos/config.py15
-rw-r--r--python/vyos/defaults.py3
-rw-r--r--python/vyos/remote.py133
-rwxr-xr-xsrc/helpers/vyos-merge-config.py96
4 files changed, 246 insertions, 1 deletions
diff --git a/python/vyos/config.py b/python/vyos/config.py
index bcf04225b..9a5125eb9 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -169,6 +169,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:
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 36185f16a..0603efc42 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -15,7 +15,8 @@
directories = {
- "data": "/usr/share/vyos/"
+ "data": "/usr/share/vyos/",
+ "config": "/opt/vyatta/etc/config"
}
cfg_group = 'vyattacfg'
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
new file mode 100644
index 000000000..372780c91
--- /dev/null
+++ b/python/vyos/remote.py
@@ -0,0 +1,133 @@
+# 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'))
+
+ 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/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py
new file mode 100755
index 000000000..f0d5d1595
--- /dev/null
+++ b/src/helpers/vyos-merge-config.py
@@ -0,0 +1,96 @@
+#!/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 vyos.defaults
+import vyos.remote
+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)
+
+path = None
+if (len(sys.argv) > 2):
+ path = " ".join(sys.argv[2:])
+
+merge_config_tree = ConfigTree(config_file)
+
+effective_config = Config()
+
+output_effective_config = effective_config.show_config()
+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 ]
+
+if path:
+ if not effective_config.exists(path):
+ print("path {} does not exist in running config; will use "
+ "root.".format(path))
+ else:
+ 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.")