diff options
| -rw-r--r-- | interface-definitions/service_config-sync.xml.in | 6 | ||||
| -rw-r--r-- | python/vyos/configsession.py | 19 | ||||
| -rw-r--r-- | python/vyos/configtree.py | 24 | ||||
| -rwxr-xr-x | src/helpers/vyos_config_sync.py | 66 | ||||
| -rwxr-xr-x | src/services/vyos-http-api-server | 38 | 
5 files changed, 112 insertions, 41 deletions
| diff --git a/interface-definitions/service_config-sync.xml.in b/interface-definitions/service_config-sync.xml.in index cb51a33b1..e9ea9aa4b 100644 --- a/interface-definitions/service_config-sync.xml.in +++ b/interface-definitions/service_config-sync.xml.in @@ -495,6 +495,12 @@                        <valueless/>                      </properties>                    </leafNode> +                  <leafNode name="time-zone"> +                    <properties> +                      <help>Local time zone</help> +                      <valueless/> +                    </properties> +                  </leafNode>                  </children>                </node>                <leafNode name="vpn"> 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/python/vyos/configtree.py b/python/vyos/configtree.py index 423fe01ed..e4b282d72 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -401,6 +401,30 @@ def union(left, right, libpath=LIBPATH):      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") + +    try: +        __lib = cdll.LoadLibrary(libpath) +        __mask_tree = __lib.mask_tree +        __mask_tree.argtypes = [c_void_p, c_void_p] +        __mask_tree.restype = c_void_p +        __get_error = __lib.get_error +        __get_error.argtypes = [] +        __get_error.restype = c_char_p + +        res = __mask_tree(left._get_config(), right._get_config()) +    except Exception as e: +        raise ConfigTreeError(e) +    if not res: +        msg = __get_error().decode() +        raise ConfigTreeError(msg) + +    tree = ConfigTree(address=res) + +    return tree +  def reference_tree_to_json(from_dir, to_file, libpath=LIBPATH):      try:          __lib = cdll.LoadLibrary(libpath) diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 77f7cd810..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, Union, 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) | 
