Skip to content

Commit

Permalink
config-sync: T6185: combine data for sections/configs in one command
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jestabro committed Mar 29, 2024
1 parent b2248b6 commit 30a5308
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 41 deletions.
19 changes: 19 additions & 0 deletions python/vyos/configsession.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ def load_section(self, path: list, d: dict):
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 = [""]
Expand Down
66 changes: 34 additions & 32 deletions src/helpers/vyos_config_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
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


Expand Down Expand Up @@ -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
Expand All @@ -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
})

Expand Down Expand Up @@ -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}")

Expand Down
38 changes: 29 additions & 9 deletions src/services/vyos-http-api-server
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit 30a5308

Please sign in to comment.