From 5e333ccf4a4ddabbb61042d5b347de54845d60c0 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 11 Jun 2025 11:50:21 -0500 Subject: T7499: add interface for (non-)destructive configtree merge --- python/vyos/configtree.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 9b3755841..dd7dc5b93 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -499,6 +499,28 @@ def union(left, right, libpath=LIBPATH): return tree +def merge(left, right, destructive=False, libpath=LIBPATH): + if left is None: + left = ConfigTree(config_string='\n') + if right is None: + right = ConfigTree(config_string='\n') + if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)): + raise TypeError('Arguments must be instances of ConfigTree') + + __lib = cdll.LoadLibrary(libpath) + __tree_merge = __lib.tree_merge + __tree_merge.argtypes = [c_bool, c_void_p, c_void_p] + __tree_merge.restype = c_void_p + __get_error = __lib.get_error + __get_error.argtypes = [] + __get_error.restype = c_char_p + + res = __tree_merge(destructive, left._get_config(), right._get_config()) + tree = ConfigTree(address=res) + + return tree + + def mask_inclusive(left, right, libpath=LIBPATH): if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)): raise TypeError('Arguments must be instances of ConfigTree') -- cgit v1.2.3 From 05db4cdef55acc1a5869dabaab60264074c30c7d Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 23 Jun 2025 13:54:05 -0500 Subject: T7499: update vyos-merge-config.py script to use tree merge function --- python/vyos/config.py | 2 + src/helpers/vyos-merge-config.py | 128 +++++++++++++++++++-------------------- 2 files changed, 63 insertions(+), 67 deletions(-) diff --git a/python/vyos/config.py b/python/vyos/config.py index 6ba8834cb..6f7c76ca7 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -134,9 +134,11 @@ class Config(object): subtrees. """ def __init__(self, session_env=None, config_source=None): + self.vyconf_session = None if config_source is None: if vyconf_backend() and boot_configuration_complete(): self._config_source = ConfigSourceVyconfSession(session_env) + self.vyconf_session = self._config_source._vyconf_session else: self._config_source = ConfigSourceSession(session_env) else: diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index 79b17a261..197b35bfa 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -2,10 +2,6 @@ # Copyright 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 @@ -15,94 +11,92 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . +import os import sys +import shlex import tempfile -import vyos.defaults -import vyos.remote +import argparse +from vyos.defaults import directories +from vyos.remote import get_remote_config 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 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() + +file_name = args.config_file +paths = [shlex.split(s) for s in args.paths] if args.paths else [] + +configdir = 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.") +if any(file_name.startswith(f'{x}://') for x in protocols): + file_path = get_remote_config(file_name) + if not file_path: + sys.exit(f'No such file {file_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) + if os.path.isfile(file_name): + file_path = file_name + else: + file_path = os.path.join(configdir, file_name) + if not os.path.isfile(file_path): + sys.exit(f'No such file {file_name}') + +if args.migrate: + migrate = ConfigMigrate(file_path) try: - config_migrate.run() + migrate.run() except ConfigMigrateError as e: sys.exit(e) -merge_config_tree = ConfigTree(config_file) +with open(file_path) as f: + merge_str = f.read() -effective_config = Config() -effective_config_tree = effective_config._running_config +merge_ct = ConfigTree(merge_str) -effective_cmds = effective_config_tree.to_commands() -merge_cmds = merge_config_tree.to_commands() +if paths: + mask = ConfigTree('') + for p in paths: + mask.set(p) -effective_cmd_list = effective_cmds.splitlines() -merge_cmd_list = merge_cmds.splitlines() + merge_ct = mask_inclusive(merge_ct, mask) -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() +session_ct = config.get_config_tree() -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) +merge_res = merge(session_ct, merge_ct, destructive=args.destructive) -if path: - add_cmds = [ cmd for cmd in add_cmds if path in cmd ] +if config.vyconf_session is not None: + with tempfile.NamedTemporaryFile() as merged_file: + with open(merged_file, 'w') as f: + f.write(merge_res.to_string()) + + out, err = config.vyconf_session.load_config(merged_file) + if err: + sys.exit(out) + print(out) +else: + 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) -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.') -- cgit v1.2.3 From 8e84c690e59fc27d601c24261442ce34e00af1f2 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 23 Jun 2025 19:02:15 -0500 Subject: T7499: fix typo in configtree write_cache --- python/vyos/configtree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index dd7dc5b93..ba3f1e368 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -233,7 +233,7 @@ class ConfigTree(object): return self.__version def write_cache(self, file_name): - self.__write_internal(self._get_config(), file_name) + self.__write_internal(self._get_config(), file_name.encode()) def to_string(self, ordered_values=False, no_version=False): config_string = self.__to_string(self.__config, ordered_values).decode() -- cgit v1.2.3 From 500c150bf049421b74dd6bc6a3e55c3e2600cc62 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 23 Jun 2025 14:46:33 -0500 Subject: T7499: load from internal representation to avoid re-parsing --- python/vyos/vyconf_session.py | 8 ++++++-- src/helpers/vyos-merge-config.py | 7 +++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py index 4a2e6e393..3f3f3957b 100644 --- a/python/vyos/vyconf_session.py +++ b/python/vyos/vyconf_session.py @@ -163,7 +163,9 @@ class VyconfSession: @raise_exception @config_mode - def load_config(self, file: str, migrate: bool = False) -> tuple[str, int]: + def load_config( + self, file: str, migrate: bool = False, cached: bool = False + ) -> tuple[str, int]: # pylint: disable=consider-using-with if migrate: tmp = tempfile.NamedTemporaryFile() @@ -178,7 +180,9 @@ class VyconfSession: else: tmp = '' - out = vyconf_client.send_request('load', token=self.__token, location=file) + out = vyconf_client.send_request( + 'load', token=self.__token, location=file, cached=cached + ) if tmp: tmp.close() diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index 197b35bfa..9ac4a1aed 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -84,11 +84,10 @@ session_ct = config.get_config_tree() merge_res = merge(session_ct, merge_ct, destructive=args.destructive) if config.vyconf_session is not None: - with tempfile.NamedTemporaryFile() as merged_file: - with open(merged_file, 'w') as f: - f.write(merge_res.to_string()) + with tempfile.NamedTemporaryFile() as merged_cache: + merge_res.write_cache(merged_cache.name) - out, err = config.vyconf_session.load_config(merged_file) + out, err = config.vyconf_session.load_config(merged_cache.name, cached=True) if err: sys.exit(out) print(out) -- cgit v1.2.3 From a9b42b38ae85f38b3d60d2386549d37bd40e50ce Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 23 Jun 2025 15:12:00 -0500 Subject: T7499: generated output for adding field 'cached' to load function --- python/vyos/proto/vyconf_pb2.py | 70 +++++++++++++++++++-------------------- python/vyos/proto/vyconf_proto.py | 5 +-- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/python/vyos/proto/vyconf_pb2.py b/python/vyos/proto/vyconf_pb2.py index 3d5042888..f3b62977e 100644 --- a/python/vyos/proto/vyconf_pb2.py +++ b/python/vyos/proto/vyconf_pb2.py @@ -13,17 +13,17 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cvyconf.proto\"\x89\x15\n\x07Request\x12!\n\x06prompt\x18\x01 \x01(\x0b\x32\x0f.Request.PromptH\x00\x12.\n\rsetup_session\x18\x02 \x01(\x0b\x32\x15.Request.SetupSessionH\x00\x12\x1b\n\x03set\x18\x03 \x01(\x0b\x32\x0c.Request.SetH\x00\x12!\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x0f.Request.DeleteH\x00\x12!\n\x06rename\x18\x05 \x01(\x0b\x32\x0f.Request.RenameH\x00\x12\x1d\n\x04\x63opy\x18\x06 \x01(\x0b\x32\r.Request.CopyH\x00\x12#\n\x07\x63omment\x18\x07 \x01(\x0b\x32\x10.Request.CommentH\x00\x12!\n\x06\x63ommit\x18\x08 \x01(\x0b\x32\x0f.Request.CommitH\x00\x12%\n\x08rollback\x18\t \x01(\x0b\x32\x11.Request.RollbackH\x00\x12\x1f\n\x05merge\x18\n \x01(\x0b\x32\x0e.Request.MergeH\x00\x12\x1d\n\x04save\x18\x0b \x01(\x0b\x32\r.Request.SaveH\x00\x12*\n\x0bshow_config\x18\x0c \x01(\x0b\x32\x13.Request.ShowConfigH\x00\x12!\n\x06\x65xists\x18\r \x01(\x0b\x32\x0f.Request.ExistsH\x00\x12&\n\tget_value\x18\x0e \x01(\x0b\x32\x11.Request.GetValueH\x00\x12(\n\nget_values\x18\x0f \x01(\x0b\x32\x12.Request.GetValuesH\x00\x12.\n\rlist_children\x18\x10 \x01(\x0b\x32\x15.Request.ListChildrenH\x00\x12)\n\x0brun_op_mode\x18\x11 \x01(\x0b\x32\x12.Request.RunOpModeH\x00\x12#\n\x07\x63onfirm\x18\x12 \x01(\x0b\x32\x10.Request.ConfirmH\x00\x12\x43\n\x18\x65nter_configuration_mode\x18\x13 \x01(\x0b\x32\x1f.Request.EnterConfigurationModeH\x00\x12\x41\n\x17\x65xit_configuration_mode\x18\x14 \x01(\x0b\x32\x1e.Request.ExitConfigurationModeH\x00\x12%\n\x08validate\x18\x15 \x01(\x0b\x32\x11.Request.ValidateH\x00\x12%\n\x08teardown\x18\x16 \x01(\x0b\x32\x11.Request.TeardownH\x00\x12\x30\n\x0ereload_reftree\x18\x17 \x01(\x0b\x32\x16.Request.ReloadReftreeH\x00\x12\x1d\n\x04load\x18\x18 \x01(\x0b\x32\r.Request.LoadH\x00\x12#\n\x07\x64iscard\x18\x19 \x01(\x0b\x32\x10.Request.DiscardH\x00\x12\x32\n\x0fsession_changed\x18\x1a \x01(\x0b\x32\x17.Request.SessionChangedH\x00\x12/\n\x0esession_of_pid\x18\x1b \x01(\x0b\x32\x15.Request.SessionOfPidH\x00\x12\x37\n\x12session_update_pid\x18\x1c \x01(\x0b\x32\x19.Request.SessionUpdatePidH\x00\x12(\n\nget_config\x18\x1d \x01(\x0b\x32\x12.Request.GetConfigH\x00\x1a\x08\n\x06Prompt\x1aP\n\x0cSetupSession\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x12\x19\n\x11\x43lientApplication\x18\x02 \x01(\t\x12\x12\n\nOnBehalfOf\x18\x03 \x01(\x05\x1a!\n\x0cSessionOfPid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a%\n\x10SessionUpdatePid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a\x1a\n\tGetConfig\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1e\n\x08Teardown\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\x1a\x46\n\x08Validate\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\x13\n\x03Set\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x16\n\x06\x44\x65lete\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x18\n\x07\x44iscard\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1f\n\x0eSessionChanged\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x35\n\x06Rename\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a\x33\n\x04\x43opy\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a(\n\x07\x43omment\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12\x0f\n\x07\x43omment\x18\x02 \x02(\t\x1aR\n\x06\x43ommit\x12\x0f\n\x07\x43onfirm\x18\x01 \x01(\x08\x12\x16\n\x0e\x43onfirmTimeout\x18\x02 \x01(\x05\x12\x0f\n\x07\x43omment\x18\x03 \x01(\t\x12\x0e\n\x06\x44ryRun\x18\x04 \x01(\x08\x1a\x1c\n\x08Rollback\x12\x10\n\x08Revision\x18\x01 \x02(\x05\x1a?\n\x04Load\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a@\n\x05Merge\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a?\n\x04Save\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x41\n\nShowConfig\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x16\n\x06\x45xists\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x46\n\x08GetValue\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tGetValues\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aJ\n\x0cListChildren\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tRunOpMode\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\t\n\x07\x43onfirm\x1a\x46\n\x16\x45nterConfigurationMode\x12\x11\n\tExclusive\x18\x01 \x02(\x08\x12\x19\n\x11OverrideExclusive\x18\x02 \x02(\x08\x1a\x17\n\x15\x45xitConfigurationMode\x1a#\n\rReloadReftree\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\"#\n\x0c\x43onfigFormat\x12\t\n\x05\x43URLY\x10\x00\x12\x08\n\x04JSON\x10\x01\")\n\x0cOutputFormat\x12\x0c\n\x08OutPlain\x10\x00\x12\x0b\n\x07OutJSON\x10\x01\x42\x05\n\x03msg\";\n\x0fRequestEnvelope\x12\r\n\x05token\x18\x01 \x01(\t\x12\x19\n\x07request\x18\x02 \x02(\x0b\x32\x08.Request\"S\n\x08Response\x12\x17\n\x06status\x18\x01 \x02(\x0e\x32\x07.Errnum\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\x0f\n\x07warning\x18\x04 \x01(\t*\xd2\x01\n\x06\x45rrnum\x12\x0b\n\x07SUCCESS\x10\x00\x12\x08\n\x04\x46\x41IL\x10\x01\x12\x10\n\x0cINVALID_PATH\x10\x02\x12\x11\n\rINVALID_VALUE\x10\x03\x12\x16\n\x12\x43OMMIT_IN_PROGRESS\x10\x04\x12\x18\n\x14\x43ONFIGURATION_LOCKED\x10\x05\x12\x12\n\x0eINTERNAL_ERROR\x10\x06\x12\x15\n\x11PERMISSION_DENIED\x10\x07\x12\x17\n\x13PATH_ALREADY_EXISTS\x10\x08\x12\x16\n\x12UNCOMMITED_CHANGES\x10\t') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cvyconf.proto\"\x99\x15\n\x07Request\x12!\n\x06prompt\x18\x01 \x01(\x0b\x32\x0f.Request.PromptH\x00\x12.\n\rsetup_session\x18\x02 \x01(\x0b\x32\x15.Request.SetupSessionH\x00\x12\x1b\n\x03set\x18\x03 \x01(\x0b\x32\x0c.Request.SetH\x00\x12!\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x0f.Request.DeleteH\x00\x12!\n\x06rename\x18\x05 \x01(\x0b\x32\x0f.Request.RenameH\x00\x12\x1d\n\x04\x63opy\x18\x06 \x01(\x0b\x32\r.Request.CopyH\x00\x12#\n\x07\x63omment\x18\x07 \x01(\x0b\x32\x10.Request.CommentH\x00\x12!\n\x06\x63ommit\x18\x08 \x01(\x0b\x32\x0f.Request.CommitH\x00\x12%\n\x08rollback\x18\t \x01(\x0b\x32\x11.Request.RollbackH\x00\x12\x1f\n\x05merge\x18\n \x01(\x0b\x32\x0e.Request.MergeH\x00\x12\x1d\n\x04save\x18\x0b \x01(\x0b\x32\r.Request.SaveH\x00\x12*\n\x0bshow_config\x18\x0c \x01(\x0b\x32\x13.Request.ShowConfigH\x00\x12!\n\x06\x65xists\x18\r \x01(\x0b\x32\x0f.Request.ExistsH\x00\x12&\n\tget_value\x18\x0e \x01(\x0b\x32\x11.Request.GetValueH\x00\x12(\n\nget_values\x18\x0f \x01(\x0b\x32\x12.Request.GetValuesH\x00\x12.\n\rlist_children\x18\x10 \x01(\x0b\x32\x15.Request.ListChildrenH\x00\x12)\n\x0brun_op_mode\x18\x11 \x01(\x0b\x32\x12.Request.RunOpModeH\x00\x12#\n\x07\x63onfirm\x18\x12 \x01(\x0b\x32\x10.Request.ConfirmH\x00\x12\x43\n\x18\x65nter_configuration_mode\x18\x13 \x01(\x0b\x32\x1f.Request.EnterConfigurationModeH\x00\x12\x41\n\x17\x65xit_configuration_mode\x18\x14 \x01(\x0b\x32\x1e.Request.ExitConfigurationModeH\x00\x12%\n\x08validate\x18\x15 \x01(\x0b\x32\x11.Request.ValidateH\x00\x12%\n\x08teardown\x18\x16 \x01(\x0b\x32\x11.Request.TeardownH\x00\x12\x30\n\x0ereload_reftree\x18\x17 \x01(\x0b\x32\x16.Request.ReloadReftreeH\x00\x12\x1d\n\x04load\x18\x18 \x01(\x0b\x32\r.Request.LoadH\x00\x12#\n\x07\x64iscard\x18\x19 \x01(\x0b\x32\x10.Request.DiscardH\x00\x12\x32\n\x0fsession_changed\x18\x1a \x01(\x0b\x32\x17.Request.SessionChangedH\x00\x12/\n\x0esession_of_pid\x18\x1b \x01(\x0b\x32\x15.Request.SessionOfPidH\x00\x12\x37\n\x12session_update_pid\x18\x1c \x01(\x0b\x32\x19.Request.SessionUpdatePidH\x00\x12(\n\nget_config\x18\x1d \x01(\x0b\x32\x12.Request.GetConfigH\x00\x1a\x08\n\x06Prompt\x1aP\n\x0cSetupSession\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x12\x19\n\x11\x43lientApplication\x18\x02 \x01(\t\x12\x12\n\nOnBehalfOf\x18\x03 \x01(\x05\x1a!\n\x0cSessionOfPid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a%\n\x10SessionUpdatePid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a\x1a\n\tGetConfig\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1e\n\x08Teardown\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\x1a\x46\n\x08Validate\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\x13\n\x03Set\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x16\n\x06\x44\x65lete\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x18\n\x07\x44iscard\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1f\n\x0eSessionChanged\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x35\n\x06Rename\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a\x33\n\x04\x43opy\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a(\n\x07\x43omment\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12\x0f\n\x07\x43omment\x18\x02 \x02(\t\x1aR\n\x06\x43ommit\x12\x0f\n\x07\x43onfirm\x18\x01 \x01(\x08\x12\x16\n\x0e\x43onfirmTimeout\x18\x02 \x01(\x05\x12\x0f\n\x07\x43omment\x18\x03 \x01(\t\x12\x0e\n\x06\x44ryRun\x18\x04 \x01(\x08\x1a\x1c\n\x08Rollback\x12\x10\n\x08Revision\x18\x01 \x02(\x05\x1aO\n\x04Load\x12\x10\n\x08Location\x18\x01 \x02(\t\x12\x0e\n\x06\x63\x61\x63hed\x18\x02 \x02(\x08\x12%\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a@\n\x05Merge\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a?\n\x04Save\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x41\n\nShowConfig\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x16\n\x06\x45xists\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x46\n\x08GetValue\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tGetValues\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aJ\n\x0cListChildren\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tRunOpMode\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\t\n\x07\x43onfirm\x1a\x46\n\x16\x45nterConfigurationMode\x12\x11\n\tExclusive\x18\x01 \x02(\x08\x12\x19\n\x11OverrideExclusive\x18\x02 \x02(\x08\x1a\x17\n\x15\x45xitConfigurationMode\x1a#\n\rReloadReftree\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\"#\n\x0c\x43onfigFormat\x12\t\n\x05\x43URLY\x10\x00\x12\x08\n\x04JSON\x10\x01\")\n\x0cOutputFormat\x12\x0c\n\x08OutPlain\x10\x00\x12\x0b\n\x07OutJSON\x10\x01\x42\x05\n\x03msg\";\n\x0fRequestEnvelope\x12\r\n\x05token\x18\x01 \x01(\t\x12\x19\n\x07request\x18\x02 \x02(\x0b\x32\x08.Request\"S\n\x08Response\x12\x17\n\x06status\x18\x01 \x02(\x0e\x32\x07.Errnum\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\x0f\n\x07warning\x18\x04 \x01(\t*\xd2\x01\n\x06\x45rrnum\x12\x0b\n\x07SUCCESS\x10\x00\x12\x08\n\x04\x46\x41IL\x10\x01\x12\x10\n\x0cINVALID_PATH\x10\x02\x12\x11\n\rINVALID_VALUE\x10\x03\x12\x16\n\x12\x43OMMIT_IN_PROGRESS\x10\x04\x12\x18\n\x14\x43ONFIGURATION_LOCKED\x10\x05\x12\x12\n\x0eINTERNAL_ERROR\x10\x06\x12\x15\n\x11PERMISSION_DENIED\x10\x07\x12\x17\n\x13PATH_ALREADY_EXISTS\x10\x08\x12\x16\n\x12UNCOMMITED_CHANGES\x10\t') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'vyconf_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _ERRNUM._serialized_start=2863 - _ERRNUM._serialized_end=3073 + _ERRNUM._serialized_start=2879 + _ERRNUM._serialized_end=3089 _REQUEST._serialized_start=17 - _REQUEST._serialized_end=2714 + _REQUEST._serialized_end=2730 _REQUEST_PROMPT._serialized_start=1237 _REQUEST_PROMPT._serialized_end=1245 _REQUEST_SETUPSESSION._serialized_start=1247 @@ -57,37 +57,37 @@ if _descriptor._USE_C_DESCRIPTORS == False: _REQUEST_ROLLBACK._serialized_start=1873 _REQUEST_ROLLBACK._serialized_end=1901 _REQUEST_LOAD._serialized_start=1903 - _REQUEST_LOAD._serialized_end=1966 - _REQUEST_MERGE._serialized_start=1968 - _REQUEST_MERGE._serialized_end=2032 - _REQUEST_SAVE._serialized_start=2034 - _REQUEST_SAVE._serialized_end=2097 - _REQUEST_SHOWCONFIG._serialized_start=2099 - _REQUEST_SHOWCONFIG._serialized_end=2164 - _REQUEST_EXISTS._serialized_start=2166 - _REQUEST_EXISTS._serialized_end=2188 - _REQUEST_GETVALUE._serialized_start=2190 - _REQUEST_GETVALUE._serialized_end=2260 - _REQUEST_GETVALUES._serialized_start=2262 - _REQUEST_GETVALUES._serialized_end=2333 - _REQUEST_LISTCHILDREN._serialized_start=2335 - _REQUEST_LISTCHILDREN._serialized_end=2409 - _REQUEST_RUNOPMODE._serialized_start=2411 - _REQUEST_RUNOPMODE._serialized_end=2482 + _REQUEST_LOAD._serialized_end=1982 + _REQUEST_MERGE._serialized_start=1984 + _REQUEST_MERGE._serialized_end=2048 + _REQUEST_SAVE._serialized_start=2050 + _REQUEST_SAVE._serialized_end=2113 + _REQUEST_SHOWCONFIG._serialized_start=2115 + _REQUEST_SHOWCONFIG._serialized_end=2180 + _REQUEST_EXISTS._serialized_start=2182 + _REQUEST_EXISTS._serialized_end=2204 + _REQUEST_GETVALUE._serialized_start=2206 + _REQUEST_GETVALUE._serialized_end=2276 + _REQUEST_GETVALUES._serialized_start=2278 + _REQUEST_GETVALUES._serialized_end=2349 + _REQUEST_LISTCHILDREN._serialized_start=2351 + _REQUEST_LISTCHILDREN._serialized_end=2425 + _REQUEST_RUNOPMODE._serialized_start=2427 + _REQUEST_RUNOPMODE._serialized_end=2498 _REQUEST_CONFIRM._serialized_start=1799 _REQUEST_CONFIRM._serialized_end=1808 - _REQUEST_ENTERCONFIGURATIONMODE._serialized_start=2495 - _REQUEST_ENTERCONFIGURATIONMODE._serialized_end=2565 - _REQUEST_EXITCONFIGURATIONMODE._serialized_start=2567 - _REQUEST_EXITCONFIGURATIONMODE._serialized_end=2590 - _REQUEST_RELOADREFTREE._serialized_start=2592 - _REQUEST_RELOADREFTREE._serialized_end=2627 - _REQUEST_CONFIGFORMAT._serialized_start=2629 - _REQUEST_CONFIGFORMAT._serialized_end=2664 - _REQUEST_OUTPUTFORMAT._serialized_start=2666 - _REQUEST_OUTPUTFORMAT._serialized_end=2707 - _REQUESTENVELOPE._serialized_start=2716 - _REQUESTENVELOPE._serialized_end=2775 - _RESPONSE._serialized_start=2777 - _RESPONSE._serialized_end=2860 + _REQUEST_ENTERCONFIGURATIONMODE._serialized_start=2511 + _REQUEST_ENTERCONFIGURATIONMODE._serialized_end=2581 + _REQUEST_EXITCONFIGURATIONMODE._serialized_start=2583 + _REQUEST_EXITCONFIGURATIONMODE._serialized_end=2606 + _REQUEST_RELOADREFTREE._serialized_start=2608 + _REQUEST_RELOADREFTREE._serialized_end=2643 + _REQUEST_CONFIGFORMAT._serialized_start=2645 + _REQUEST_CONFIGFORMAT._serialized_end=2680 + _REQUEST_OUTPUTFORMAT._serialized_start=2682 + _REQUEST_OUTPUTFORMAT._serialized_end=2723 + _REQUESTENVELOPE._serialized_start=2732 + _REQUESTENVELOPE._serialized_end=2791 + _RESPONSE._serialized_start=2793 + _RESPONSE._serialized_end=2876 # @@protoc_insertion_point(module_scope) diff --git a/python/vyos/proto/vyconf_proto.py b/python/vyos/proto/vyconf_proto.py index 404ef2f27..4f820af45 100644 --- a/python/vyos/proto/vyconf_proto.py +++ b/python/vyos/proto/vyconf_proto.py @@ -100,6 +100,7 @@ class Rollback: @dataclass class Load: Location: str = "" + cached: bool = False format: ConfigFormat = None @dataclass @@ -298,8 +299,8 @@ def set_request_rollback(token: str = None, revision: int = 0): req_env = RequestEnvelope(token, req) return req_env -def set_request_load(token: str = None, location: str = "", format: ConfigFormat = None): - reqi = Load (location, format) +def set_request_load(token: str = None, location: str = "", cached: bool = False, format: ConfigFormat = None): + reqi = Load (location, cached, format) req = Request(load=reqi) req_env = RequestEnvelope(token, req) return req_env -- cgit v1.2.3 From 8af85206e172b2c50514df9990ea6061f72a1ed5 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 23 Jun 2025 15:20:37 -0500 Subject: T7499: add unittest for config tree merge function --- src/tests/test_config_merge.py | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/tests/test_config_merge.py 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 +# +# 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 . + +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'])) -- cgit v1.2.3 From 170244db88f76e42aeceb0b971246327fe079e19 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 23 Jun 2025 19:24:08 -0500 Subject: T7499: expose destructive merge in http-api --- python/vyos/configsession.py | 8 +++----- src/services/api/rest/models.py | 1 + src/services/api/rest/routers.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 216069992..175b40260 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -363,11 +363,9 @@ class ConfigSession(object): return out - def merge_config(self, file_path): - if self._vyconf_session is None: - out = self.__run_command(MERGE_CONFIG + [file_path]) - else: - out = 'unimplemented' + def merge_config(self, file_path, destructive=False): + destr = ['--destructive'] if destructive else [] + out = self.__run_command(MERGE_CONFIG + [file_path] + destr) return out 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) -- cgit v1.2.3 From 816834bcad0a536930dd1bc4a35ef6b8537f3522 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 24 Jun 2025 07:11:41 -0500 Subject: T7499: use direct request to vyconfd to avoid re-validating --- python/vyos/vyconf_session.py | 27 +++++++++++++++++++++++++++ src/helpers/vyos-merge-config.py | 25 ++++++++++++------------- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py index 3f3f3957b..b42266793 100644 --- a/python/vyos/vyconf_session.py +++ b/python/vyos/vyconf_session.py @@ -188,6 +188,33 @@ class VyconfSession: return self.output(out), out.status + @raise_exception + @config_mode + def merge_config( + self, file: str, migrate: bool = False, destructive: bool = False + ) -> tuple[str, int]: + # pylint: disable=consider-using-with + if migrate: + tmp = tempfile.NamedTemporaryFile() + shutil.copy2(file, tmp.name) + config_migrate = ConfigMigrate(tmp.name) + try: + config_migrate.run() + except ConfigMigrateError as e: + tmp.close() + return repr(e), 1 + file = tmp.name + else: + tmp = '' + + out = vyconf_client.send_request( + 'merge', token=self.__token, location=file, destructive=destructive + ) + if tmp: + tmp.close() + + return self.output(out), out.status + @raise_exception def save_config(self, file: str, append_version: bool = False) -> tuple[str, int]: out = vyconf_client.send_request('save', token=self.__token, location=file) diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index 9ac4a1aed..4879b0e9c 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -14,7 +14,6 @@ import os import sys import shlex -import tempfile import argparse from vyos.defaults import directories @@ -52,8 +51,9 @@ if any(file_name.startswith(f'{x}://') for x in protocols): if not file_path: sys.exit(f'No such file {file_name}') else: - if os.path.isfile(file_name): - file_path = file_name + full_path = os.path.realpath(file_name) + if os.path.isfile(full_path): + file_path = full_path else: file_path = os.path.join(configdir, file_name) if not os.path.isfile(file_path): @@ -79,19 +79,18 @@ if paths: merge_ct = mask_inclusive(merge_ct, mask) config = Config() -session_ct = config.get_config_tree() - -merge_res = merge(session_ct, merge_ct, destructive=args.destructive) if config.vyconf_session is not None: - with tempfile.NamedTemporaryFile() as merged_cache: - merge_res.write_cache(merged_cache.name) - - out, err = config.vyconf_session.load_config(merged_cache.name, cached=True) - if err: - sys.exit(out) - print(out) + out, err = config.vyconf_session.merge_config( + file_path, destructive=args.destructive + ) + if err: + sys.exit(out) + print(out) else: + session_ct = config.get_config_tree() + merge_res = merge(session_ct, merge_ct, destructive=args.destructive) + load_explicit(merge_res) -- cgit v1.2.3 From 352ebebc689df69488a70ff496f364f03d4767fa Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 24 Jun 2025 07:55:40 -0500 Subject: T7499: generated output for adding field 'destructive' to merge function --- python/vyos/proto/vyconf_pb2.py | 66 +++++++++++++++++++-------------------- python/vyos/proto/vyconf_proto.py | 5 +-- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/python/vyos/proto/vyconf_pb2.py b/python/vyos/proto/vyconf_pb2.py index f3b62977e..4bf0eb2e0 100644 --- a/python/vyos/proto/vyconf_pb2.py +++ b/python/vyos/proto/vyconf_pb2.py @@ -13,17 +13,17 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cvyconf.proto\"\x99\x15\n\x07Request\x12!\n\x06prompt\x18\x01 \x01(\x0b\x32\x0f.Request.PromptH\x00\x12.\n\rsetup_session\x18\x02 \x01(\x0b\x32\x15.Request.SetupSessionH\x00\x12\x1b\n\x03set\x18\x03 \x01(\x0b\x32\x0c.Request.SetH\x00\x12!\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x0f.Request.DeleteH\x00\x12!\n\x06rename\x18\x05 \x01(\x0b\x32\x0f.Request.RenameH\x00\x12\x1d\n\x04\x63opy\x18\x06 \x01(\x0b\x32\r.Request.CopyH\x00\x12#\n\x07\x63omment\x18\x07 \x01(\x0b\x32\x10.Request.CommentH\x00\x12!\n\x06\x63ommit\x18\x08 \x01(\x0b\x32\x0f.Request.CommitH\x00\x12%\n\x08rollback\x18\t \x01(\x0b\x32\x11.Request.RollbackH\x00\x12\x1f\n\x05merge\x18\n \x01(\x0b\x32\x0e.Request.MergeH\x00\x12\x1d\n\x04save\x18\x0b \x01(\x0b\x32\r.Request.SaveH\x00\x12*\n\x0bshow_config\x18\x0c \x01(\x0b\x32\x13.Request.ShowConfigH\x00\x12!\n\x06\x65xists\x18\r \x01(\x0b\x32\x0f.Request.ExistsH\x00\x12&\n\tget_value\x18\x0e \x01(\x0b\x32\x11.Request.GetValueH\x00\x12(\n\nget_values\x18\x0f \x01(\x0b\x32\x12.Request.GetValuesH\x00\x12.\n\rlist_children\x18\x10 \x01(\x0b\x32\x15.Request.ListChildrenH\x00\x12)\n\x0brun_op_mode\x18\x11 \x01(\x0b\x32\x12.Request.RunOpModeH\x00\x12#\n\x07\x63onfirm\x18\x12 \x01(\x0b\x32\x10.Request.ConfirmH\x00\x12\x43\n\x18\x65nter_configuration_mode\x18\x13 \x01(\x0b\x32\x1f.Request.EnterConfigurationModeH\x00\x12\x41\n\x17\x65xit_configuration_mode\x18\x14 \x01(\x0b\x32\x1e.Request.ExitConfigurationModeH\x00\x12%\n\x08validate\x18\x15 \x01(\x0b\x32\x11.Request.ValidateH\x00\x12%\n\x08teardown\x18\x16 \x01(\x0b\x32\x11.Request.TeardownH\x00\x12\x30\n\x0ereload_reftree\x18\x17 \x01(\x0b\x32\x16.Request.ReloadReftreeH\x00\x12\x1d\n\x04load\x18\x18 \x01(\x0b\x32\r.Request.LoadH\x00\x12#\n\x07\x64iscard\x18\x19 \x01(\x0b\x32\x10.Request.DiscardH\x00\x12\x32\n\x0fsession_changed\x18\x1a \x01(\x0b\x32\x17.Request.SessionChangedH\x00\x12/\n\x0esession_of_pid\x18\x1b \x01(\x0b\x32\x15.Request.SessionOfPidH\x00\x12\x37\n\x12session_update_pid\x18\x1c \x01(\x0b\x32\x19.Request.SessionUpdatePidH\x00\x12(\n\nget_config\x18\x1d \x01(\x0b\x32\x12.Request.GetConfigH\x00\x1a\x08\n\x06Prompt\x1aP\n\x0cSetupSession\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x12\x19\n\x11\x43lientApplication\x18\x02 \x01(\t\x12\x12\n\nOnBehalfOf\x18\x03 \x01(\x05\x1a!\n\x0cSessionOfPid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a%\n\x10SessionUpdatePid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a\x1a\n\tGetConfig\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1e\n\x08Teardown\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\x1a\x46\n\x08Validate\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\x13\n\x03Set\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x16\n\x06\x44\x65lete\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x18\n\x07\x44iscard\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1f\n\x0eSessionChanged\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x35\n\x06Rename\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a\x33\n\x04\x43opy\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a(\n\x07\x43omment\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12\x0f\n\x07\x43omment\x18\x02 \x02(\t\x1aR\n\x06\x43ommit\x12\x0f\n\x07\x43onfirm\x18\x01 \x01(\x08\x12\x16\n\x0e\x43onfirmTimeout\x18\x02 \x01(\x05\x12\x0f\n\x07\x43omment\x18\x03 \x01(\t\x12\x0e\n\x06\x44ryRun\x18\x04 \x01(\x08\x1a\x1c\n\x08Rollback\x12\x10\n\x08Revision\x18\x01 \x02(\x05\x1aO\n\x04Load\x12\x10\n\x08Location\x18\x01 \x02(\t\x12\x0e\n\x06\x63\x61\x63hed\x18\x02 \x02(\x08\x12%\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a@\n\x05Merge\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a?\n\x04Save\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x41\n\nShowConfig\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x16\n\x06\x45xists\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x46\n\x08GetValue\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tGetValues\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aJ\n\x0cListChildren\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tRunOpMode\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\t\n\x07\x43onfirm\x1a\x46\n\x16\x45nterConfigurationMode\x12\x11\n\tExclusive\x18\x01 \x02(\x08\x12\x19\n\x11OverrideExclusive\x18\x02 \x02(\x08\x1a\x17\n\x15\x45xitConfigurationMode\x1a#\n\rReloadReftree\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\"#\n\x0c\x43onfigFormat\x12\t\n\x05\x43URLY\x10\x00\x12\x08\n\x04JSON\x10\x01\")\n\x0cOutputFormat\x12\x0c\n\x08OutPlain\x10\x00\x12\x0b\n\x07OutJSON\x10\x01\x42\x05\n\x03msg\";\n\x0fRequestEnvelope\x12\r\n\x05token\x18\x01 \x01(\t\x12\x19\n\x07request\x18\x02 \x02(\x0b\x32\x08.Request\"S\n\x08Response\x12\x17\n\x06status\x18\x01 \x02(\x0e\x32\x07.Errnum\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\x0f\n\x07warning\x18\x04 \x01(\t*\xd2\x01\n\x06\x45rrnum\x12\x0b\n\x07SUCCESS\x10\x00\x12\x08\n\x04\x46\x41IL\x10\x01\x12\x10\n\x0cINVALID_PATH\x10\x02\x12\x11\n\rINVALID_VALUE\x10\x03\x12\x16\n\x12\x43OMMIT_IN_PROGRESS\x10\x04\x12\x18\n\x14\x43ONFIGURATION_LOCKED\x10\x05\x12\x12\n\x0eINTERNAL_ERROR\x10\x06\x12\x15\n\x11PERMISSION_DENIED\x10\x07\x12\x17\n\x13PATH_ALREADY_EXISTS\x10\x08\x12\x16\n\x12UNCOMMITED_CHANGES\x10\t') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cvyconf.proto\"\xae\x15\n\x07Request\x12!\n\x06prompt\x18\x01 \x01(\x0b\x32\x0f.Request.PromptH\x00\x12.\n\rsetup_session\x18\x02 \x01(\x0b\x32\x15.Request.SetupSessionH\x00\x12\x1b\n\x03set\x18\x03 \x01(\x0b\x32\x0c.Request.SetH\x00\x12!\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x0f.Request.DeleteH\x00\x12!\n\x06rename\x18\x05 \x01(\x0b\x32\x0f.Request.RenameH\x00\x12\x1d\n\x04\x63opy\x18\x06 \x01(\x0b\x32\r.Request.CopyH\x00\x12#\n\x07\x63omment\x18\x07 \x01(\x0b\x32\x10.Request.CommentH\x00\x12!\n\x06\x63ommit\x18\x08 \x01(\x0b\x32\x0f.Request.CommitH\x00\x12%\n\x08rollback\x18\t \x01(\x0b\x32\x11.Request.RollbackH\x00\x12\x1f\n\x05merge\x18\n \x01(\x0b\x32\x0e.Request.MergeH\x00\x12\x1d\n\x04save\x18\x0b \x01(\x0b\x32\r.Request.SaveH\x00\x12*\n\x0bshow_config\x18\x0c \x01(\x0b\x32\x13.Request.ShowConfigH\x00\x12!\n\x06\x65xists\x18\r \x01(\x0b\x32\x0f.Request.ExistsH\x00\x12&\n\tget_value\x18\x0e \x01(\x0b\x32\x11.Request.GetValueH\x00\x12(\n\nget_values\x18\x0f \x01(\x0b\x32\x12.Request.GetValuesH\x00\x12.\n\rlist_children\x18\x10 \x01(\x0b\x32\x15.Request.ListChildrenH\x00\x12)\n\x0brun_op_mode\x18\x11 \x01(\x0b\x32\x12.Request.RunOpModeH\x00\x12#\n\x07\x63onfirm\x18\x12 \x01(\x0b\x32\x10.Request.ConfirmH\x00\x12\x43\n\x18\x65nter_configuration_mode\x18\x13 \x01(\x0b\x32\x1f.Request.EnterConfigurationModeH\x00\x12\x41\n\x17\x65xit_configuration_mode\x18\x14 \x01(\x0b\x32\x1e.Request.ExitConfigurationModeH\x00\x12%\n\x08validate\x18\x15 \x01(\x0b\x32\x11.Request.ValidateH\x00\x12%\n\x08teardown\x18\x16 \x01(\x0b\x32\x11.Request.TeardownH\x00\x12\x30\n\x0ereload_reftree\x18\x17 \x01(\x0b\x32\x16.Request.ReloadReftreeH\x00\x12\x1d\n\x04load\x18\x18 \x01(\x0b\x32\r.Request.LoadH\x00\x12#\n\x07\x64iscard\x18\x19 \x01(\x0b\x32\x10.Request.DiscardH\x00\x12\x32\n\x0fsession_changed\x18\x1a \x01(\x0b\x32\x17.Request.SessionChangedH\x00\x12/\n\x0esession_of_pid\x18\x1b \x01(\x0b\x32\x15.Request.SessionOfPidH\x00\x12\x37\n\x12session_update_pid\x18\x1c \x01(\x0b\x32\x19.Request.SessionUpdatePidH\x00\x12(\n\nget_config\x18\x1d \x01(\x0b\x32\x12.Request.GetConfigH\x00\x1a\x08\n\x06Prompt\x1aP\n\x0cSetupSession\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x12\x19\n\x11\x43lientApplication\x18\x02 \x01(\t\x12\x12\n\nOnBehalfOf\x18\x03 \x01(\x05\x1a!\n\x0cSessionOfPid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a%\n\x10SessionUpdatePid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a\x1a\n\tGetConfig\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1e\n\x08Teardown\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\x1a\x46\n\x08Validate\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\x13\n\x03Set\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x16\n\x06\x44\x65lete\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x18\n\x07\x44iscard\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1f\n\x0eSessionChanged\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x35\n\x06Rename\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a\x33\n\x04\x43opy\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a(\n\x07\x43omment\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12\x0f\n\x07\x43omment\x18\x02 \x02(\t\x1aR\n\x06\x43ommit\x12\x0f\n\x07\x43onfirm\x18\x01 \x01(\x08\x12\x16\n\x0e\x43onfirmTimeout\x18\x02 \x01(\x05\x12\x0f\n\x07\x43omment\x18\x03 \x01(\t\x12\x0e\n\x06\x44ryRun\x18\x04 \x01(\x08\x1a\x1c\n\x08Rollback\x12\x10\n\x08Revision\x18\x01 \x02(\x05\x1aO\n\x04Load\x12\x10\n\x08Location\x18\x01 \x02(\t\x12\x0e\n\x06\x63\x61\x63hed\x18\x02 \x02(\x08\x12%\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x15.Request.ConfigFormat\x1aU\n\x05Merge\x12\x10\n\x08Location\x18\x01 \x02(\t\x12\x13\n\x0b\x64\x65structive\x18\x02 \x02(\x08\x12%\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a?\n\x04Save\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x41\n\nShowConfig\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x16\n\x06\x45xists\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x46\n\x08GetValue\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tGetValues\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aJ\n\x0cListChildren\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tRunOpMode\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\t\n\x07\x43onfirm\x1a\x46\n\x16\x45nterConfigurationMode\x12\x11\n\tExclusive\x18\x01 \x02(\x08\x12\x19\n\x11OverrideExclusive\x18\x02 \x02(\x08\x1a\x17\n\x15\x45xitConfigurationMode\x1a#\n\rReloadReftree\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\"#\n\x0c\x43onfigFormat\x12\t\n\x05\x43URLY\x10\x00\x12\x08\n\x04JSON\x10\x01\")\n\x0cOutputFormat\x12\x0c\n\x08OutPlain\x10\x00\x12\x0b\n\x07OutJSON\x10\x01\x42\x05\n\x03msg\";\n\x0fRequestEnvelope\x12\r\n\x05token\x18\x01 \x01(\t\x12\x19\n\x07request\x18\x02 \x02(\x0b\x32\x08.Request\"S\n\x08Response\x12\x17\n\x06status\x18\x01 \x02(\x0e\x32\x07.Errnum\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\x0f\n\x07warning\x18\x04 \x01(\t*\xd2\x01\n\x06\x45rrnum\x12\x0b\n\x07SUCCESS\x10\x00\x12\x08\n\x04\x46\x41IL\x10\x01\x12\x10\n\x0cINVALID_PATH\x10\x02\x12\x11\n\rINVALID_VALUE\x10\x03\x12\x16\n\x12\x43OMMIT_IN_PROGRESS\x10\x04\x12\x18\n\x14\x43ONFIGURATION_LOCKED\x10\x05\x12\x12\n\x0eINTERNAL_ERROR\x10\x06\x12\x15\n\x11PERMISSION_DENIED\x10\x07\x12\x17\n\x13PATH_ALREADY_EXISTS\x10\x08\x12\x16\n\x12UNCOMMITED_CHANGES\x10\t') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'vyconf_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _ERRNUM._serialized_start=2879 - _ERRNUM._serialized_end=3089 + _ERRNUM._serialized_start=2900 + _ERRNUM._serialized_end=3110 _REQUEST._serialized_start=17 - _REQUEST._serialized_end=2730 + _REQUEST._serialized_end=2751 _REQUEST_PROMPT._serialized_start=1237 _REQUEST_PROMPT._serialized_end=1245 _REQUEST_SETUPSESSION._serialized_start=1247 @@ -59,35 +59,35 @@ if _descriptor._USE_C_DESCRIPTORS == False: _REQUEST_LOAD._serialized_start=1903 _REQUEST_LOAD._serialized_end=1982 _REQUEST_MERGE._serialized_start=1984 - _REQUEST_MERGE._serialized_end=2048 - _REQUEST_SAVE._serialized_start=2050 - _REQUEST_SAVE._serialized_end=2113 - _REQUEST_SHOWCONFIG._serialized_start=2115 - _REQUEST_SHOWCONFIG._serialized_end=2180 - _REQUEST_EXISTS._serialized_start=2182 - _REQUEST_EXISTS._serialized_end=2204 - _REQUEST_GETVALUE._serialized_start=2206 - _REQUEST_GETVALUE._serialized_end=2276 - _REQUEST_GETVALUES._serialized_start=2278 - _REQUEST_GETVALUES._serialized_end=2349 - _REQUEST_LISTCHILDREN._serialized_start=2351 - _REQUEST_LISTCHILDREN._serialized_end=2425 - _REQUEST_RUNOPMODE._serialized_start=2427 - _REQUEST_RUNOPMODE._serialized_end=2498 + _REQUEST_MERGE._serialized_end=2069 + _REQUEST_SAVE._serialized_start=2071 + _REQUEST_SAVE._serialized_end=2134 + _REQUEST_SHOWCONFIG._serialized_start=2136 + _REQUEST_SHOWCONFIG._serialized_end=2201 + _REQUEST_EXISTS._serialized_start=2203 + _REQUEST_EXISTS._serialized_end=2225 + _REQUEST_GETVALUE._serialized_start=2227 + _REQUEST_GETVALUE._serialized_end=2297 + _REQUEST_GETVALUES._serialized_start=2299 + _REQUEST_GETVALUES._serialized_end=2370 + _REQUEST_LISTCHILDREN._serialized_start=2372 + _REQUEST_LISTCHILDREN._serialized_end=2446 + _REQUEST_RUNOPMODE._serialized_start=2448 + _REQUEST_RUNOPMODE._serialized_end=2519 _REQUEST_CONFIRM._serialized_start=1799 _REQUEST_CONFIRM._serialized_end=1808 - _REQUEST_ENTERCONFIGURATIONMODE._serialized_start=2511 - _REQUEST_ENTERCONFIGURATIONMODE._serialized_end=2581 - _REQUEST_EXITCONFIGURATIONMODE._serialized_start=2583 - _REQUEST_EXITCONFIGURATIONMODE._serialized_end=2606 - _REQUEST_RELOADREFTREE._serialized_start=2608 - _REQUEST_RELOADREFTREE._serialized_end=2643 - _REQUEST_CONFIGFORMAT._serialized_start=2645 - _REQUEST_CONFIGFORMAT._serialized_end=2680 - _REQUEST_OUTPUTFORMAT._serialized_start=2682 - _REQUEST_OUTPUTFORMAT._serialized_end=2723 - _REQUESTENVELOPE._serialized_start=2732 - _REQUESTENVELOPE._serialized_end=2791 - _RESPONSE._serialized_start=2793 - _RESPONSE._serialized_end=2876 + _REQUEST_ENTERCONFIGURATIONMODE._serialized_start=2532 + _REQUEST_ENTERCONFIGURATIONMODE._serialized_end=2602 + _REQUEST_EXITCONFIGURATIONMODE._serialized_start=2604 + _REQUEST_EXITCONFIGURATIONMODE._serialized_end=2627 + _REQUEST_RELOADREFTREE._serialized_start=2629 + _REQUEST_RELOADREFTREE._serialized_end=2664 + _REQUEST_CONFIGFORMAT._serialized_start=2666 + _REQUEST_CONFIGFORMAT._serialized_end=2701 + _REQUEST_OUTPUTFORMAT._serialized_start=2703 + _REQUEST_OUTPUTFORMAT._serialized_end=2744 + _REQUESTENVELOPE._serialized_start=2753 + _REQUESTENVELOPE._serialized_end=2812 + _RESPONSE._serialized_start=2814 + _RESPONSE._serialized_end=2897 # @@protoc_insertion_point(module_scope) diff --git a/python/vyos/proto/vyconf_proto.py b/python/vyos/proto/vyconf_proto.py index 4f820af45..ec62a6e35 100644 --- a/python/vyos/proto/vyconf_proto.py +++ b/python/vyos/proto/vyconf_proto.py @@ -106,6 +106,7 @@ class Load: @dataclass class Merge: Location: str = "" + destructive: bool = False format: ConfigFormat = None @dataclass @@ -305,8 +306,8 @@ def set_request_load(token: str = None, location: str = "", cached: bool = False req_env = RequestEnvelope(token, req) return req_env -def set_request_merge(token: str = None, location: str = "", format: ConfigFormat = None): - reqi = Merge (location, format) +def set_request_merge(token: str = None, location: str = "", destructive: bool = False, format: ConfigFormat = None): + reqi = Merge (location, destructive, format) req = Request(merge=reqi) req_env = RequestEnvelope(token, req) return req_env -- cgit v1.2.3 From fc7329f3d2bb6b18a36b01626d1fab51bde4012e Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sat, 28 Jun 2025 23:05:00 -0500 Subject: T7499: expose direct request to http-api --- python/vyos/configsession.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 175b40260..569a82f3c 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -364,8 +364,13 @@ class ConfigSession(object): return out def merge_config(self, file_path, destructive=False): - destr = ['--destructive'] if destructive else [] - out = self.__run_command(MERGE_CONFIG + [file_path] + destr) + if self._vyconf_session is None: + destr = ['--destructive'] if destructive else [] + out = self.__run_command(MERGE_CONFIG + [file_path] + destr) + else: + out, _ = self._vyconf_session.merge_config( + file=file_path, destructive=destructive + ) return out -- cgit v1.2.3 From b43e3d7dd90c72decc9a01d58608878503a60212 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sat, 28 Jun 2025 23:05:49 -0500 Subject: T7499: formatting --- python/vyos/configsession.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 569a82f3c..63111afe4 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -182,8 +182,9 @@ class ConfigSession(object): self.__run_command([CLI_SHELL_API, 'setupSession']) if vyconf_backend() and boot_configuration_complete(): - self._vyconf_session = VyconfSession(pid=session_id, - on_error=ConfigSessionError) + self._vyconf_session = VyconfSession( + pid=session_id, on_error=ConfigSessionError + ) else: self._vyconf_session = None -- cgit v1.2.3 From 04a714fbf22c4accd9253f55826b0eefcd9f88fa Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 29 Jun 2025 17:11:44 -0500 Subject: T7499: add utility to download/uncompress config file, for load/merge --- python/vyos/remote.py | 44 ++++++++++++++++++ src/helpers/vyos-load-config.py | 99 +++++++++++++++------------------------- src/helpers/vyos-merge-config.py | 47 +++++++++---------- 3 files changed, 104 insertions(+), 86 deletions(-) diff --git a/python/vyos/remote.py b/python/vyos/remote.py index f6ab5c3f9..b73f486c0 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -22,6 +22,7 @@ import stat import sys import tempfile import urllib.parse +import gzip from contextlib import contextmanager from pathlib import Path @@ -44,6 +45,7 @@ from vyos.utils.misc import begin from vyos.utils.process import cmd, rc_cmd from vyos.version import get_version from vyos.base import Warning +from vyos.defaults import directories CHUNK_SIZE = 8192 @@ -478,3 +480,45 @@ def get_remote_config(urlstring, source_host='', source_port=0): return f.read() finally: os.remove(temp) + + +def get_config_file(file_in: str, file_out: str, source_host='', source_port=0): + protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] + config_dir = directories['config'] + + with tempfile.NamedTemporaryFile() as tmp_file: + if any(file_in.startswith(f'{x}://') for x in protocols): + try: + download( + tmp_file.name, + file_in, + check_space=True, + source_host='', + source_port=0, + raise_error=True, + ) + except Exception as e: + return e + file_name = tmp_file.name + else: + full_path = os.path.realpath(file_in) + if os.path.isfile(full_path): + file_in = full_path + else: + file_in = os.path.join(config_dir, file_in) + if not os.path.isfile(file_in): + return ValueError(f'No such file {file_in}') + + file_name = file_in + + if file_in.endswith('.gz'): + try: + with gzip.open(file_name, 'rb') as f_in: + with open(file_out, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + except Exception as e: + return e + else: + shutil.copyfile(file_name, file_out) + + return None 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 4879b0e9c..e8a696eb5 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -2,22 +2,27 @@ # Copyright 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 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 . +# # -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see . import os import sys import shlex import argparse +import tempfile -from vyos.defaults import directories -from vyos.remote import get_remote_config +from vyos.remote import get_config_file from vyos.config import Config from vyos.configtree import ConfigTree from vyos.configtree import mask_inclusive @@ -42,28 +47,19 @@ args = parser.parse_args() file_name = args.config_file paths = [shlex.split(s) for s in args.paths] if args.paths else [] -configdir = directories['config'] - -protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] - -if any(file_name.startswith(f'{x}://') for x in protocols): - file_path = get_remote_config(file_name) - if not file_path: - sys.exit(f'No such file {file_name}') -else: - full_path = os.path.realpath(file_name) - if os.path.isfile(full_path): - file_path = full_path - else: - file_path = os.path.join(configdir, file_name) - if not os.path.isfile(file_path): - sys.exit(f'No such file {file_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: migrate.run() except ConfigMigrateError as e: + os.remove(file_path) sys.exit(e) with open(file_path) as f: @@ -78,6 +74,9 @@ if paths: merge_ct = mask_inclusive(merge_ct, mask) +with open(file_path, 'w') as f: + f.write(merge_ct.to_string()) + config = Config() if config.vyconf_session is not None: @@ -85,6 +84,7 @@ if config.vyconf_session is not None: file_path, destructive=args.destructive ) if err: + os.remove(file_path) sys.exit(out) print(out) else: @@ -93,6 +93,7 @@ else: load_explicit(merge_res) +os.remove(file_path) if config.session_changed(): print("Merge complete. Use 'commit' to make changes effective.") -- cgit v1.2.3 From 2dfb0865149b8a71d095f34e7b2d050c4c143c7f Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 29 Jun 2025 17:39:33 -0500 Subject: T7499: add download/uncompress to vyconf load/merge --- python/vyos/configsession.py | 6 ++--- python/vyos/vyconf_session.py | 58 ++++++++++++++++++++++--------------------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 63111afe4..50f93f890 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -343,7 +343,7 @@ class ConfigSession(object): if self._vyconf_session is None: out = self.__run_command(LOAD_CONFIG + [file_path]) else: - out, _ = self._vyconf_session.load_config(file=file_path) + out, _ = self._vyconf_session.load_config(file_name=file_path) return out @@ -360,7 +360,7 @@ class ConfigSession(object): if self._vyconf_session is None: out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) else: - out, _ = self._vyconf_session.load_config(file=file_path, migrate=True) + out, _ = self._vyconf_session.load_config(file_name=file_path, migrate=True) return out @@ -370,7 +370,7 @@ class ConfigSession(object): out = self.__run_command(MERGE_CONFIG + [file_path] + destr) else: out, _ = self._vyconf_session.merge_config( - file=file_path, destructive=destructive + file_name=file_path, destructive=destructive ) return out diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py index b42266793..f564e0c25 100644 --- a/python/vyos/vyconf_session.py +++ b/python/vyos/vyconf_session.py @@ -17,7 +17,6 @@ import os import tempfile -import shutil from functools import wraps from typing import Type @@ -30,6 +29,7 @@ from vyos.proto.vyconf_proto import Errnum from vyos.utils.commit import acquire_commit_lock_file from vyos.utils.commit import release_commit_lock_file from vyos.utils.commit import call_commit_hooks +from vyos.remote import get_config_file class VyconfSessionError(Exception): @@ -164,54 +164,56 @@ class VyconfSession: @raise_exception @config_mode def load_config( - self, file: str, migrate: bool = False, cached: bool = False + self, file_name: str, migrate: bool = False, cached: bool = False ) -> tuple[str, int]: # pylint: disable=consider-using-with - if migrate: - tmp = tempfile.NamedTemporaryFile() - shutil.copy2(file, tmp.name) - config_migrate = ConfigMigrate(tmp.name) - try: - config_migrate.run() - except ConfigMigrateError as e: - tmp.close() - return repr(e), 1 - file = tmp.name - else: - tmp = '' + file_path = tempfile.NamedTemporaryFile(delete=False).name + err = get_config_file(file_name, file_path) + if err: + os.remove(file_path) + return str(err), Errnum.INVALID_VALUE + if not cached: + if migrate: + config_migrate = ConfigMigrate(file_path) + try: + config_migrate.run() + except ConfigMigrateError as e: + os.remove(file_path) + return repr(e), 1 out = vyconf_client.send_request( - 'load', token=self.__token, location=file, cached=cached + 'load', token=self.__token, location=file_path, cached=cached ) - if tmp: - tmp.close() + + if not cached: + os.remove(file_path) return self.output(out), out.status @raise_exception @config_mode def merge_config( - self, file: str, migrate: bool = False, destructive: bool = False + self, file_name: str, migrate: bool = False, destructive: bool = False ) -> tuple[str, int]: # 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) + return str(err), Errnum.INVALID_VALUE if migrate: - tmp = tempfile.NamedTemporaryFile() - shutil.copy2(file, tmp.name) - config_migrate = ConfigMigrate(tmp.name) + config_migrate = ConfigMigrate(file_path) try: config_migrate.run() except ConfigMigrateError as e: - tmp.close() + os.remove(file_path) return repr(e), 1 - file = tmp.name - else: - tmp = '' out = vyconf_client.send_request( - 'merge', token=self.__token, location=file, destructive=destructive + 'merge', token=self.__token, location=file_path, destructive=destructive ) - if tmp: - tmp.close() + + os.remove(file_path) return self.output(out), out.status -- cgit v1.2.3 From fa184c3357977fa47f7108c7c99bd8920c43af94 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Fri, 4 Jul 2025 17:15:45 -0500 Subject: T7499: clean up cache files --- python/vyos/configsource.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py index 3931f1295..949216722 100644 --- a/python/vyos/configsource.py +++ b/python/vyos/configsource.py @@ -345,6 +345,11 @@ class ConfigSourceVyconfSession(ConfigSource): self._running_config = ConfigTree(internal=self.running_cache_path) self._session_config = ConfigTree(internal=self.session_cache_path) + if os.path.isfile(self.running_cache_path): + os.remove(self.running_cache_path) + if os.path.isfile(self.session_cache_path): + os.remove(self.session_cache_path) + # N.B. level not yet implemented pending integration with legacy CLI # cf. T7374 self._level = [] -- cgit v1.2.3 From 9203e6445829fdeadfd86d7d86548729aa91ce35 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 7 Jul 2025 19:01:09 -0500 Subject: T7499: call commit only if session_changed --- python/vyos/vyconf_session.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py index f564e0c25..3cf5fb4e3 100644 --- a/python/vyos/vyconf_session.py +++ b/python/vyos/vyconf_session.py @@ -142,6 +142,10 @@ class VyconfSession: @raise_exception @config_mode def commit(self) -> tuple[str, int]: + if not self.session_changed(): + out = 'No changes to commit' + return out, 0 + lock_fd, out = acquire_commit_lock_file() if lock_fd is None: return out, Errnum.COMMIT_IN_PROGRESS -- cgit v1.2.3 From 4d7a3a972a11ead1386ae2719b70dbfc2411831f Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 8 Jul 2025 08:10:55 -0500 Subject: T7499: update submodule for vyconf/vyos-1x changes --- libvyosconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libvyosconfig b/libvyosconfig index f632edbc9..73600f99a 160000 --- a/libvyosconfig +++ b/libvyosconfig @@ -1 +1 @@ -Subproject commit f632edbc947fbcda1916ababacc5f2659cf6cfb8 +Subproject commit 73600f99aa11f01e3f8148da2212abf51a766632 -- cgit v1.2.3