diff options
-rw-r--r-- | op-mode-definitions/container.xml.in | 35 | ||||
-rw-r--r-- | python/vyos/configverify.py | 27 | ||||
-rw-r--r-- | python/vyos/frr.py | 16 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_protocols_bgp.py | 7 | ||||
-rwxr-xr-x | src/helpers/vyos_config_sync.py | 48 | ||||
-rwxr-xr-x | src/services/vyos-http-api-server | 2 |
6 files changed, 98 insertions, 37 deletions
diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index 4aa13e913..bb6f97b02 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -103,12 +103,28 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/container.py show_container</command> <children> - <leafNode name="image"> + <node name="json"> + <properties> + <help>Show containers in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_container --raw</command> + </node> + <node name="image"> <properties> <help>Show container image</help> </properties> <command>sudo ${vyos_op_scripts_dir}/container.py show_image</command> - </leafNode> + <children> + <node name="json"> + <properties> + <help>Show container image in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_image --raw</command> + </node> + </children> + </node> <tagNode name="log"> <properties> <help>Show logs from a given container</help> @@ -116,14 +132,25 @@ <path>container name</path> </completionHelp> </properties> + <!-- no admin check --> <command>sudo podman logs --names "$4"</command> </tagNode> - <leafNode name="network"> + <node name="network"> <properties> <help>Show available container networks</help> </properties> + <!-- no admin check --> <command>sudo ${vyos_op_scripts_dir}/container.py show_network</command> - </leafNode> + <children> + <node name="json"> + <properties> + <help>Show available container networks in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_network --raw</command> + </node> + </children> + </node> </children> </node> <node name="log"> diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 5d3723876..6508ccdd9 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -269,14 +269,33 @@ def verify_bridge_delete(config): raise ConfigError(f'Interface "{interface}" cannot be deleted as it ' f'is a member of bridge "{bridge_name}"!') -def verify_interface_exists(ifname): +def verify_interface_exists(ifname, warning_only=False): """ Common helper function used by interface implementations to perform - recurring validation if an interface actually exists. + recurring validation if an interface actually exists. We first probe + if the interface is defined on the CLI, if it's not found we try if + it exists at the OS level. """ import os - if not os.path.exists(f'/sys/class/net/{ifname}'): - raise ConfigError(f'Interface "{ifname}" does not exist!') + from vyos.base import Warning + from vyos.configquery import ConfigTreeQuery + from vyos.utils.dict import dict_search_recursive + + # Check if interface is present in CLI config + config = ConfigTreeQuery() + tmp = config.get_config_dict(['interfaces'], get_first_key=True) + if bool(list(dict_search_recursive(tmp, ifname))): + return True + + # Interface not found on CLI, try Linux Kernel + if os.path.exists(f'/sys/class/net/{ifname}'): + return True + + message = f'Interface "{ifname}" does not exist!' + if warning_only: + Warning(message) + return False + raise ConfigError(message) def verify_source_interface(config): """ diff --git a/python/vyos/frr.py b/python/vyos/frr.py index a01d967e4..c3703cbb4 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -68,6 +68,7 @@ Apply the new configuration: import tempfile import re +from vyos import ConfigError from vyos.utils.permission import chown from vyos.utils.process import cmd from vyos.utils.process import popen @@ -95,6 +96,7 @@ path_config = '/run/frr' default_add_before = r'(ip prefix-list .*|route-map .*|line vty|end)' + class FrrError(Exception): pass @@ -210,13 +212,12 @@ def reload_configuration(config, daemon=None): LOG.debug(f'reload_configuration: Executing command against frr-reload: "{cmd}"') output, code = popen(cmd, stderr=STDOUT) f.close() + for i, e in enumerate(output.split('\n')): LOG.debug(f'frr-reload output: {i:3} {e}') + if code == 1: - raise CommitError('FRR configuration failed while running commit. Please ' \ - 'enable debugging to examine logs.\n\n\n' \ - 'To enable debugging run: "touch /tmp/vyos.frr.debug" ' \ - 'and "sudo systemctl stop vyos-configd"') + raise ConfigError(output) elif code: raise OSError(code, output) @@ -469,17 +470,22 @@ class FRRConfig: # https://github.com/FRRouting/frr/issues/10133 count = 0 count_max = 5 + emsg = '' while count < count_max: count += 1 try: reload_configuration('\n'.join(self.config), daemon=daemon) break + except ConfigError as e: + emsg = str(e) except: # we just need to re-try the commit of the configuration # for the listed FRR issues above pass if count >= count_max: - raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} dameon!') + if emsg: + raise ConfigError(emsg) + raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} daemon!') # Save configuration to /run/frr/config/frr.conf save_configuration() diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 1d68ae08b..1dc865977 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -1236,6 +1236,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {pg_ipv6} activate', afiv6_config) self.assertIn(f' neighbor {pg_ipv6} maximum-prefix {ipv6_max_prefix}', afiv6_config) + def test_bgp_26_commit_error(self): + self.cli_set(base_path + ['peer-group', 'peer1', 'address-family', 'l2vpn-evpn', 'route-reflector-client']) + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + + self.assertTrue("% Invalid command. Not an internal neighbor" in str(e.exception)) + def test_bgp_99_bmp(self): target_name = 'instance-bmp' target_address = '127.0.0.1' diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 572fea61f..77f7cd810 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -61,14 +61,16 @@ def post_request(url: str, -def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]: +def retrieve_config(section: Optional[List[str]] = None) -> Optional[Dict[str, Any]]: """Retrieves the configuration from the local server. Args: - section: str: The section of the configuration to retrieve. Default is None. + section: List[str]: The section of the configuration to retrieve. + Default is None. Returns: - Optional[Dict[str, Any]]: The retrieved configuration as a dictionary, or None if an error occurred. + Optional[Dict[str, Any]]: The retrieved configuration as a + dictionary, or None if an error occurred. """ if section is None: section = [] @@ -83,23 +85,21 @@ def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]: def set_remote_config( address: str, key: str, - op: str, - path: str = None, - section: Optional[str] = None) -> Optional[Dict[str, Any]]: + commands: List[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. - path (Optional[str]): The path of the configuration. Default is None. - section (Optional[str]): The section of the configuration to load. Default is None. + commands (list): List of set/load commands for request, given as: + [{'op': str, 'path': list[str], 'section': dict}, + ...] Returns: - Optional[Dict[str, Any]]: The response from the remote host as a dictionary, or None if an error occurred. + Optional[Dict[str, Any]]: The response from the remote host as a + dictionary, or None if a RequestException occurred. """ - if path is None: - path = [] headers = {'Content-Type': 'application/json'} # Disable the InsecureRequestWarning @@ -107,9 +107,7 @@ def set_remote_config( url = f'https://{address}/configure-section' data = json.dumps({ - 'op': mode, - 'path': path, - 'section': section, + 'commands': commands, 'key': key }) @@ -122,14 +120,14 @@ def set_remote_config( return None -def is_section_revised(section: str) -> bool: +def is_section_revised(section: List[str]) -> bool: from vyos.config_mgmt import is_node_revised return is_node_revised(section) def config_sync(secondary_address: str, secondary_key: str, - sections: List[list], + sections: List[list[str]], mode: str): """Retrieve a config section from primary router in JSON format and send it to secondary router @@ -142,21 +140,25 @@ 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 = "" + config_json = {} logger.debug( f"Retrieved config for section '{section}': {config_json}") - set_config = set_remote_config(address=secondary_address, - key=secondary_key, - op=mode, - path=section, - section=config_json) - logger.debug(f"Set config for section '{section}': {set_config}") + + d = {'op': mode, 'path': section, 'section': config_json} + commands.append(d) + + set_config = set_remote_config(address=secondary_address, + key=secondary_key, + commands=commands) + + logger.debug(f"Set config for sections '{sections}': {set_config}") if __name__ == '__main__': diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index a7b14a1a3..77870a84c 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -463,7 +463,7 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel, endpoint = request.url.path # Allow users to pass just one command - if not isinstance(data, ConfigureListModel): + if not isinstance(data, (ConfigureListModel, ConfigSectionListModel)): data = [data] else: data = data.commands |