summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorViacheslav Hletenko <v.gletenko@vyos.io>2024-03-31 15:00:28 +0300
committerGitHub <noreply@github.com>2024-03-31 15:00:28 +0300
commitf62827c80466dc7ae74a2f8a1b360f59603bc3fa (patch)
tree25fd9193764bc84ba6f22ec30f0e9de7359e6af9
parent252d03d6e419aae14ae75caed38d1b1001c916a2 (diff)
parentb6c5e66cc44fdec21e6731d98a1065e2adf87b3b (diff)
downloadvyos-1x-f62827c80466dc7ae74a2f8a1b360f59603bc3fa.tar.gz
vyos-1x-f62827c80466dc7ae74a2f8a1b360f59603bc3fa.zip
Merge pull request #3211 from jestabro/tree-mask
T6185: simplify marshalling of section and config data for config-sync
-rw-r--r--interface-definitions/service_config-sync.xml.in6
-rw-r--r--python/vyos/configsession.py19
-rw-r--r--python/vyos/configtree.py24
-rwxr-xr-xsrc/helpers/vyos_config_sync.py66
-rwxr-xr-xsrc/services/vyos-http-api-server38
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)