diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/helpers/vyos-load-config.py | 99 | ||||
-rwxr-xr-x | src/helpers/vyos-merge-config.py | 141 | ||||
-rw-r--r-- | src/services/api/rest/models.py | 1 | ||||
-rw-r--r-- | src/services/api/rest/routers.py | 2 | ||||
-rw-r--r-- | src/tests/test_config_merge.py | 50 |
5 files changed, 155 insertions, 138 deletions
diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py index cd6bff0d4..01a6a88dc 100755 --- a/src/helpers/vyos-load-config.py +++ b/src/helpers/vyos-load-config.py @@ -16,84 +16,57 @@ # # -"""Load config file from within config session. -Config file specified by URI or path (without scheme prefix). -Example: load https://somewhere.net/some.config - or - load /tmp/some.config -""" - import os import sys -import gzip +import argparse import tempfile -import vyos.defaults -import vyos.remote -from vyos.configsource import ConfigSourceSession, VyOSError + +from vyos.remote import get_config_file +from vyos.config import Config from vyos.migrate import ConfigMigrate from vyos.migrate import ConfigMigrateError +from vyos.load_config import load as load_config -class LoadConfig(ConfigSourceSession): - """A subclass for calling 'loadFile'. - This does not belong in configsource.py, and only has a single caller. - """ - def load_config(self, path): - return self._run(['/bin/cli-shell-api','loadFile',path]) - -file_name = sys.argv[1] if len(sys.argv) > 1 else 'config.boot' -configdir = vyos.defaults.directories['config'] -protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] -def get_local_config(filename): - if os.path.isfile(filename): - fname = filename - elif os.path.isfile(os.path.join(configdir, filename)): - fname = os.path.join(configdir, filename) - else: - sys.exit(f"No such file '{filename}'") +parser = argparse.ArgumentParser() +parser.add_argument('config_file', help='config file to load') +parser.add_argument( + '--migrate', action='store_true', help='migrate config file before merge' +) - if fname.endswith('.gz'): - with gzip.open(fname, 'rb') as f: - try: - config_str = f.read().decode() - except OSError as e: - sys.exit(e) - else: - with open(fname, 'r') as f: - try: - config_str = f.read() - except OSError as e: - sys.exit(e) +args = parser.parse_args() - return config_str - -if any(file_name.startswith(f'{x}://') for x in protocols): - config_string = vyos.remote.get_remote_config(file_name) - if not config_string: - sys.exit(f"No such config file at '{file_name}'") -else: - config_string = get_local_config(file_name) +file_name = args.config_file -config = LoadConfig() +# pylint: disable=consider-using-with +file_path = tempfile.NamedTemporaryFile(delete=False).name +err = get_config_file(file_name, file_path) +if err: + os.remove(file_path) + sys.exit(err) -print(f"Loading configuration from '{file_name}'") +if args.migrate: + migrate = ConfigMigrate(file_path) + try: + migrate.run() + except ConfigMigrateError as e: + os.remove(file_path) + sys.exit(e) -with tempfile.NamedTemporaryFile() as fp: - with open(fp.name, 'w') as fd: - fd.write(config_string) +config = Config() - config_migrate = ConfigMigrate(fp.name) - try: - config_migrate.run() - except ConfigMigrateError as err: - sys.exit(err) +if config.vyconf_session is not None: + out, err = config.vyconf_session.load_config(file_path) + if err: + os.remove(file_path) + sys.exit(out) + print(out) +else: + load_config(file_path) - try: - config.load_config(fp.name) - except VyOSError as err: - sys.exit(err) +os.remove(file_path) if config.session_changed(): print("Load complete. Use 'commit' to make changes effective.") else: - print("No configuration changes to commit.") + print('No configuration changes to commit.') diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index 79b17a261..e8a696eb5 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -2,107 +2,100 @@ # Copyright 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 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 library is distributed in the hope that it will be useful, +# 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 -# Lesser General Public License for more details. +# 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/>. +# # -# 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 sys +import shlex +import argparse import tempfile -import vyos.defaults -import vyos.remote +from vyos.remote import get_config_file from vyos.config import Config from vyos.configtree import ConfigTree +from vyos.configtree import mask_inclusive +from vyos.configtree import merge from vyos.migrate import ConfigMigrate from vyos.migrate import ConfigMigrateError -from vyos.utils.process import cmd -from vyos.utils.process import DEVNULL +from vyos.load_config import load_explicit -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] +parser = argparse.ArgumentParser() +parser.add_argument('config_file', help='config file to merge from') +parser.add_argument( + '--destructive', action='store_true', help='replace values with those of merge file' +) +parser.add_argument('--paths', nargs='+', help='only merge from listed paths') +parser.add_argument( + '--migrate', action='store_true', help='migrate config file before merge' +) -configdir = vyos.defaults.directories['config'] +args = parser.parse_args() -protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] +file_name = args.config_file +paths = [shlex.split(s) for s in args.paths] if args.paths else [] -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) - - config_migrate = ConfigMigrate(file_to_migrate.name) +# pylint: disable=consider-using-with +file_path = tempfile.NamedTemporaryFile(delete=False).name +err = get_config_file(file_name, file_path) +if err: + os.remove(file_path) + sys.exit(err) + +if args.migrate: + migrate = ConfigMigrate(file_path) try: - config_migrate.run() + migrate.run() except ConfigMigrateError as e: + os.remove(file_path) sys.exit(e) -merge_config_tree = ConfigTree(config_file) +with open(file_path) as f: + merge_str = f.read() + +merge_ct = ConfigTree(merge_str) -effective_config = Config() -effective_config_tree = effective_config._running_config +if paths: + mask = ConfigTree('') + for p in paths: + mask.set(p) -effective_cmds = effective_config_tree.to_commands() -merge_cmds = merge_config_tree.to_commands() + merge_ct = mask_inclusive(merge_ct, mask) -effective_cmd_list = effective_cmds.splitlines() -merge_cmd_list = merge_cmds.splitlines() +with open(file_path, 'w') as f: + f.write(merge_ct.to_string()) -effective_cmd_set = set(effective_cmd_list) -add_cmds = [ cmd for cmd in merge_cmd_list if cmd not in effective_cmd_set ] +config = Config() -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 config.vyconf_session is not None: + out, err = config.vyconf_session.merge_config( + file_path, destructive=args.destructive + ) + if err: + os.remove(file_path) + sys.exit(out) + print(out) +else: + session_ct = config.get_config_tree() + merge_res = merge(session_ct, merge_ct, destructive=args.destructive) -if path: - add_cmds = [ cmd for cmd in add_cmds if path in cmd ] + load_explicit(merge_res) -for add in add_cmds: - try: - cmd(f'/opt/vyatta/sbin/my_{add}', shell=True, stderr=DEVNULL) - except OSError as err: - print(err) +os.remove(file_path) -if effective_config.session_changed(): +if config.session_changed(): print("Merge complete. Use 'commit' to make changes effective.") else: - print("No configuration changes to commit.") + print('No configuration changes to commit.') diff --git a/src/services/api/rest/models.py b/src/services/api/rest/models.py index 7a61ddfd1..70fab03ec 100644 --- a/src/services/api/rest/models.py +++ b/src/services/api/rest/models.py @@ -143,6 +143,7 @@ class ConfigFileModel(ApiModel): file: StrictStr = None string: StrictStr = None confirm_time: StrictInt = 0 + destructive: bool = False class Config: json_schema_extra = { diff --git a/src/services/api/rest/routers.py b/src/services/api/rest/routers.py index 48eca8f15..329d6e51f 100644 --- a/src/services/api/rest/routers.py +++ b/src/services/api/rest/routers.py @@ -597,7 +597,7 @@ async def config_file_op(data: ConfigFileModel, background_tasks: BackgroundTask case 'load': session.migrate_and_load_config(path) case 'merge': - session.merge_config(path) + session.merge_config(path, destructive=data.destructive) config = Config(session_env=env) d = get_config_diff(config) diff --git a/src/tests/test_config_merge.py b/src/tests/test_config_merge.py new file mode 100644 index 000000000..c9b4ad5c8 --- /dev/null +++ b/src/tests/test_config_merge.py @@ -0,0 +1,50 @@ +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 vyos.configtree + +from unittest import TestCase + +class TestConfigDiff(TestCase): + def setUp(self): + with open('tests/data/config.left', 'r') as f: + config_string = f.read() + self.config_left = vyos.configtree.ConfigTree(config_string) + + with open('tests/data/config.right', 'r') as f: + config_string = f.read() + self.config_right = vyos.configtree.ConfigTree(config_string) + + def test_merge_destructive(self): + res = vyos.configtree.merge(self.config_left, self.config_right, + destructive=True) + right_value = self.config_right.return_value(['node1', 'tag_node', 'foo', 'single']) + merge_value = res.return_value(['node1', 'tag_node', 'foo', 'single']) + + # Check includes new value + self.assertEqual(right_value, merge_value) + + # Check preserves non-confliciting paths + self.assertTrue(res.exists(['node3'])) + + def test_merge_non_destructive(self): + res = vyos.configtree.merge(self.config_left, self.config_right) + left_value = self.config_left.return_value(['node1', 'tag_node', 'foo', 'single']) + merge_value = res.return_value(['node1', 'tag_node', 'foo', 'single']) + + # Check includes original value + self.assertEqual(left_value, merge_value) + + # Check preserves non-confliciting paths + self.assertTrue(res.exists(['node3'])) |