From bd8993540c6c1fc089943052aac51c9c318ffe6f Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 28 Mar 2024 14:34:20 -0500 Subject: config-sync: T6185: combine data for sections/configs in one command Package path/section data in single command containing a tree (dict) of section paths and the accompanying config data. This drops the call to get_config_dict and the need for a list of commands in request. (cherry picked from commit 30a530839cdbd934ea62369e385dc33fa50ab6de) --- python/vyos/configsession.py | 19 +++++++++++ src/helpers/vyos_config_sync.py | 66 ++++++++++++++++++++------------------- src/services/vyos-http-api-server | 38 ++++++++++++++++------ 3 files changed, 82 insertions(+), 41 deletions(-) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 90842b749..ab7a631bb 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -176,6 +176,25 @@ class ConfigSession(object): except (ValueError, ConfigSessionError) as e: raise ConfigSessionError(e) + def set_section_tree(self, d: dict): + try: + if d: + for p in dict_to_paths(d): + self.set(p) + except (ValueError, ConfigSessionError) as e: + raise ConfigSessionError(e) + + def load_section_tree(self, mask: dict, d: dict): + try: + if mask: + for p in dict_to_paths(mask): + self.delete(p) + if d: + for p in dict_to_paths(d): + self.set(p) + except (ValueError, ConfigSessionError) as e: + raise ConfigSessionError(e) + def comment(self, path, value=None): if not value: value = [""] diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 7eec3f4f3..0604b2837 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -21,9 +21,11 @@ import json import requests import urllib3 import logging -from typing import Optional, List, Dict, Any +from typing import Optional, List, Tuple, Dict, Any from vyos.config import Config +from vyos.configtree import ConfigTree +from vyos.configtree import mask_inclusive from vyos.template import bracketize_ipv6 @@ -61,39 +63,45 @@ def post_request(url: str, -def retrieve_config(section: Optional[List[str]] = None) -> Optional[Dict[str, Any]]: +def retrieve_config(sections: List[list[str]]) -> Tuple[Dict[str, Any], Dict[str, Any]]: """Retrieves the configuration from the local server. Args: - section: List[str]: The section of the configuration to retrieve. - Default is None. + sections: List[list[str]]: The list of sections of the configuration + to retrieve, given as list of paths. Returns: - Optional[Dict[str, Any]]: The retrieved configuration as a - dictionary, or None if an error occurred. + Tuple[Dict[str, Any],Dict[str,Any]]: The tuple (mask, config) where: + - mask: The tree of paths of sections, as a dictionary. + - config: The subtree of masked config data, as a dictionary. """ - if section is None: - section = [] - conf = Config() - config = conf.get_config_dict(section, get_first_key=True) - if config: - return config - return None + mask = ConfigTree('') + for section in sections: + mask.set(section) + mask_dict = json.loads(mask.to_json()) + + config = Config() + config_tree = config.get_config_tree() + masked = mask_inclusive(config_tree, mask) + config_dict = json.loads(masked.to_json()) + return mask_dict, config_dict def set_remote_config( address: str, key: str, - commands: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + op: str, + mask: Dict[str, Any], + config: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Loads the VyOS configuration in JSON format to a remote host. Args: address (str): The address of the remote host. key (str): The key to use for loading the configuration. - commands (list): List of set/load commands for request, given as: - [{'op': str, 'path': list[str], 'section': dict}, - ...] + op (str): The operation to perform (set or load). + mask (dict): The dict of paths in sections. + config (dict): The dict of masked config data. Returns: Optional[Dict[str, Any]]: The response from the remote host as a @@ -107,7 +115,9 @@ def set_remote_config( url = f'https://{address}/configure-section' data = json.dumps({ - 'commands': commands, + 'op': op, + 'mask': mask, + 'config': config, 'key': key }) @@ -140,23 +150,15 @@ def config_sync(secondary_address: str, ) # Sync sections ("nat", "firewall", etc) - commands = [] - for section in sections: - config_json = retrieve_config(section=section) - # Check if config path deesn't exist, for example "set nat" - # we set empty value for config_json data - # As we cannot send to the remote host section "nat None" config - if not config_json: - config_json = {} - logger.debug( - f"Retrieved config for section '{section}': {config_json}") - - d = {'op': mode, 'path': section, 'section': config_json} - commands.append(d) + mask_dict, config_dict = retrieve_config(sections) + logger.debug( + f"Retrieved config for sections '{sections}': {config_dict}") set_config = set_remote_config(address=secondary_address, key=secondary_key, - commands=commands) + op=mode, + mask=mask_dict, + config=config_dict) logger.debug(f"Set config for sections '{sections}': {set_config}") diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 77870a84c..ecbf6fcf9 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -140,6 +140,14 @@ class ConfigSectionModel(ApiModel, BaseConfigSectionModel): class ConfigSectionListModel(ApiModel): commands: List[BaseConfigSectionModel] +class BaseConfigSectionTreeModel(BaseModel): + op: StrictStr + mask: Dict + config: Dict + +class ConfigSectionTreeModel(ApiModel, BaseConfigSectionTreeModel): + pass + class RetrieveModel(ApiModel): op: StrictStr path: List[StrictStr] @@ -374,7 +382,7 @@ class MultipartRequest(Request): self.form_err = (400, f"Malformed command '{c}': missing 'op' field") if endpoint not in ('/config-file', '/container-image', - '/image'): + '/image', '/configure-section'): if 'path' not in c: self.form_err = (400, f"Malformed command '{c}': missing 'path' field") @@ -392,12 +400,9 @@ class MultipartRequest(Request): self.form_err = (400, f"Malformed command '{c}': 'value' field must be a string") if endpoint in ('/configure-section'): - if 'section' not in c: - self.form_err = (400, - f"Malformed command '{c}': missing 'section' field") - elif not isinstance(c['section'], dict): + if 'section' not in c and 'config' not in c: self.form_err = (400, - f"Malformed command '{c}': 'section' field must be JSON of dict") + f"Malformed command '{c}': missing 'section' or 'config' field") if 'key' not in forms and 'key' not in merge: self.form_err = (401, "Valid API key is required") @@ -455,7 +460,8 @@ def call_commit(s: ConfigSession): logger.warning(f"ConfigSessionError: {e}") def _configure_op(data: Union[ConfigureModel, ConfigureListModel, - ConfigSectionModel, ConfigSectionListModel], + ConfigSectionModel, ConfigSectionListModel, + ConfigSectionTreeModel], request: Request, background_tasks: BackgroundTasks): session = app.state.vyos_session env = session.get_session_env() @@ -481,7 +487,8 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel, try: for c in data: op = c.op - path = c.path + if not isinstance(c, BaseConfigSectionTreeModel): + path = c.path if isinstance(c, BaseConfigureModel): if c.value: @@ -495,6 +502,10 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel, elif isinstance(c, BaseConfigSectionModel): section = c.section + elif isinstance(c, BaseConfigSectionTreeModel): + mask = c.mask + config = c.config + if isinstance(c, BaseConfigureModel): if op == 'set': session.set(path, value=value) @@ -514,6 +525,14 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel, session.load_section(path, section) else: raise ConfigSessionError(f"'{op}' is not a valid operation") + + elif isinstance(c, BaseConfigSectionTreeModel): + if op == 'set': + session.set_section_tree(config) + elif op == 'load': + session.load_section_tree(mask, config) + else: + raise ConfigSessionError(f"'{op}' is not a valid operation") # end for config = Config(session_env=env) d = get_config_diff(config) @@ -554,7 +573,8 @@ def configure_op(data: Union[ConfigureModel, @app.post('/configure-section') def configure_section_op(data: Union[ConfigSectionModel, - ConfigSectionListModel], + ConfigSectionListModel, + ConfigSectionTreeModel], request: Request, background_tasks: BackgroundTasks): return _configure_op(data, request, background_tasks) -- cgit v1.2.3