diff options
author | John Estabrook <jestabro@sentrium.io> | 2019-05-29 12:05:33 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-05-29 12:05:33 -0500 |
commit | 0a16c85a8b6aa728e142156e0e985ac21916db20 (patch) | |
tree | 2c6dabba3712f9f05662844fbd4b9994ce937a05 | |
parent | bf0f721432fa05bbc7058a0b43e2acf4ad1f30e3 (diff) | |
parent | 456abc2aa4ae10981c2aec2d2e6d975ef30fb8d6 (diff) | |
download | vyos-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.py | 15 | ||||
-rw-r--r-- | python/vyos/defaults.py | 3 | ||||
-rw-r--r-- | python/vyos/remote.py | 133 | ||||
-rwxr-xr-x | src/helpers/vyos-merge-config.py | 96 |
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.") |