diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/config_mgmt.py | 3 | ||||
-rw-r--r-- | python/vyos/configtree.py | 24 | ||||
-rw-r--r-- | python/vyos/configverify.py | 27 | ||||
-rw-r--r-- | python/vyos/ethtool.py | 24 | ||||
-rw-r--r-- | python/vyos/frr.py | 16 | ||||
-rw-r--r-- | python/vyos/ifconfig/bridge.py | 34 | ||||
-rw-r--r-- | python/vyos/ifconfig/ethernet.py | 31 | ||||
-rw-r--r-- | python/vyos/ifconfig/vti.py | 8 | ||||
-rw-r--r-- | python/vyos/opmode.py | 2 | ||||
-rw-r--r-- | python/vyos/priority.py | 75 | ||||
-rw-r--r-- | python/vyos/qos/base.py | 9 | ||||
-rw-r--r-- | python/vyos/system/compat.py | 22 | ||||
-rw-r--r-- | python/vyos/system/grub.py | 38 | ||||
-rw-r--r-- | python/vyos/template.py | 4 | ||||
-rwxr-xr-x | python/vyos/xml_ref/generate_cache.py | 3 |
15 files changed, 237 insertions, 83 deletions
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index ff078649d..28ccee769 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -132,6 +132,9 @@ class ConfigMgmt: {}).get('source_address', '') if config.exists(['system', 'host-name']): self.hostname = config.return_value(['system', 'host-name']) + if config.exists(['system', 'domain-name']): + tmp = config.return_value(['system', 'domain-name']) + self.hostname += f'.{tmp}' else: self.hostname = 'vyos' diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index d048901f0..423fe01ed 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -20,10 +20,22 @@ from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool LIBPATH = '/usr/lib/libvyosconfig.so.0' +def replace_backslash(s, search, replace): + """Modify quoted strings containing backslashes not of escape sequences""" + def replace_method(match): + result = match.group().replace(search, replace) + return result + p = re.compile(r'("[^"]*[\\][^"]*"\n|\'[^\']*[\\][^\']*\'\n)') + return p.sub(replace_method, s) + def escape_backslash(string: str) -> str: - """Escape single backslashes in string that are not in escape sequence""" - p = re.compile(r'(?<!\\)[\\](?!b|f|n|r|t|\\[^bfnrt])') - result = p.sub(r'\\\\', string) + """Escape single backslashes in quoted strings""" + result = replace_backslash(string, '\\', '\\\\') + return result + +def unescape_backslash(string: str) -> str: + """Unescape backslashes in quoted strings""" + result = replace_backslash(string, '\\\\', '\\') return result def extract_version(s): @@ -165,11 +177,14 @@ class ConfigTree(object): def to_string(self, ordered_values=False): config_string = self.__to_string(self.__config, ordered_values).decode() + config_string = unescape_backslash(config_string) config_string = "{0}\n{1}".format(config_string, self.__version) return config_string def to_commands(self, op="set"): - return self.__to_commands(self.__config, op.encode()).decode() + commands = self.__to_commands(self.__config, op.encode()).decode() + commands = unescape_backslash(commands) + return commands def to_json(self): return self.__to_json(self.__config).decode() @@ -362,6 +377,7 @@ def show_diff(left, right, path=[], commands=False, libpath=LIBPATH): msg = __get_error().decode() raise ConfigTreeError(msg) + res = unescape_backslash(res) return res def union(left, right, libpath=LIBPATH): 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/ethtool.py b/python/vyos/ethtool.py index 473c98d0c..5e241fc08 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -24,7 +24,6 @@ from vyos.utils.process import popen _drivers_without_speed_duplex_flow = ['vmxnet3', 'virtio_net', 'xen_netfront', 'iavf', 'ice', 'i40e', 'hv_netvsc', 'veth', 'ixgbevf', 'tun'] -_drivers_without_eee = ['vmxnet3', 'virtio_net', 'xen_netfront', 'hv_netvsc'] class Ethtool: """ @@ -63,8 +62,6 @@ class Ethtool: _auto_negotiation = False _auto_negotiation_supported = None _flow_control = None - _eee = False - _eee_enabled = None def __init__(self, ifname): # Get driver used for interface @@ -118,15 +115,6 @@ class Ethtool: if not bool(err): self._flow_control = loads(out) - # Get current Energy Efficient Ethernet (EEE) settings, but this is - # not supported by all NICs (e.g. vmxnet3 does not support is) - out, _ = popen(f'ethtool --show-eee {ifname}') - if len(out.splitlines()) > 1: - self._eee = True - # read current EEE setting, this returns: - # EEE status: disabled || EEE status: enabled - inactive || EEE status: enabled - active - self._eee_enabled = bool('enabled' in out.splitlines()[1]) - def check_auto_negotiation_supported(self): """ Check if the NIC supports changing auto-negotiation """ return self._auto_negotiation_supported @@ -211,15 +199,3 @@ class Ethtool: 'flow-control settings!') return 'on' if bool(self._flow_control[0]['autonegotiate']) else 'off' - - def check_eee(self): - """ Check if the NIC supports eee """ - if self.get_driver_name() in _drivers_without_eee: - return False - return self._eee - - def get_eee(self): - if self._eee_enabled == None: - raise ValueError('Interface does not support changing '\ - 'EEE settings!') - return self._eee_enabled 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/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index b29e71394..7936e3da5 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -14,12 +14,11 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. from netifaces import interfaces -import json from vyos.ifconfig.interface import Interface from vyos.utils.assertion import assert_boolean +from vyos.utils.assertion import assert_list from vyos.utils.assertion import assert_positive -from vyos.utils.process import cmd from vyos.utils.dict import dict_search from vyos.configdict import get_vlan_ids from vyos.configdict import list_diff @@ -86,6 +85,10 @@ class BridgeIf(Interface): 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/vlan_filtering', }, + 'vlan_protocol': { + 'validate': lambda v: assert_list(v, ['0x88a8', '0x8100']), + 'location': '/sys/class/net/{ifname}/bridge/vlan_protocol', + }, 'multicast_querier': { 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', @@ -248,6 +251,26 @@ class BridgeIf(Interface): """ return self.set_interface('del_port', interface) + def set_vlan_protocol(self, protocol): + """ + Set protocol used for VLAN filtering. + The valid values are 0x8100(802.1q) or 0x88A8(802.1ad). + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + + if protocol not in ['802.1q', '802.1ad']: + raise ValueError() + + map = { + '802.1ad': '0x88a8', + '802.1q' : '0x8100' + } + + return self.set_interface('vlan_protocol', map[protocol]) + def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered @@ -294,10 +317,13 @@ class BridgeIf(Interface): if member in interfaces(): self.del_port(member) - # enable/disable Vlan Filter + # enable/disable VLAN Filter tmp = '1' if 'enable_vlan' in config else '0' self.set_vlan_filter(tmp) + tmp = config.get('protocol') + self.set_vlan_protocol(tmp) + # add VLAN interfaces to local 'parent' bridge to allow forwarding if 'enable_vlan' in config: for vlan in config.get('vif_remove', {}): diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index c3f5bbf47..8d96c863f 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -404,34 +404,6 @@ class EthernetIf(Interface): print(f'could not set "{rx_tx}" ring-buffer for {ifname}') return output - def set_eee(self, enable): - """ - Enable/Disable Energy Efficient Ethernet (EEE) settings - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_eee(enable=False) - """ - if not isinstance(enable, bool): - raise ValueError('Value out of range') - - if not self.ethtool.check_eee(): - self._debug_msg(f'NIC driver does not support changing EEE settings!') - return False - - current = self.ethtool.get_eee() - if current != enable: - # Assemble command executed on system. Unfortunately there is no way - # to change this setting via sysfs - cmd = f'ethtool --set-eee {self.ifname} eee ' - cmd += 'on' if enable else 'off' - output, code = self._popen(cmd) - if code: - Warning(f'could not change "{self.ifname}" EEE setting!') - return output - return None - def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered @@ -442,9 +414,6 @@ class EthernetIf(Interface): value = 'off' if 'disable_flow_control' in config else 'on' self.set_flow_control(value) - # Always disable Energy Efficient Ethernet - self.set_eee(False) - # GRO (generic receive offload) self.set_gro(dict_search('offload.gro', config) != None) diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index 9ebbeb9ed..9511386f4 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -52,8 +52,14 @@ class VTIIf(Interface): cmd += f' {iproute2_key} {tmp}' self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly self.set_interface('admin_state', 'down') + def set_admin_state(self, state): + """ Handled outside by /etc/ipsec.d/vti-up-down """ + pass + def get_mac(self): """ Get a synthetic MAC address. """ return self.get_mac_synthetic() diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index e1af1a682..8dab9a4ca 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -81,7 +81,7 @@ class InternalError(Error): def _is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set)", name): + if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew)", name): return True else: return False diff --git a/python/vyos/priority.py b/python/vyos/priority.py new file mode 100644 index 000000000..ab4e6d411 --- /dev/null +++ b/python/vyos/priority.py @@ -0,0 +1,75 @@ +# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +from pathlib import Path +from typing import List + +from vyos.xml_ref import load_reference +from vyos.base import Warning as Warn + +def priority_data(d: dict) -> list: + def func(d, path, res, hier): + for k,v in d.items(): + if not 'node_data' in v: + continue + subpath = path + [k] + hier_prio = hier + data = v.get('node_data') + o = data.get('owner') + p = data.get('priority') + # a few interface-definitions have priority preceding owner + # attribute, instead of within properties; pass in descent + if p is not None and o is None: + hier_prio = p + if o is not None and p is None: + p = hier_prio + if o is not None and p is not None: + o = Path(o.split()[0]).name + p = int(p) + res.append((subpath, o, p)) + if isinstance(v, dict): + func(v, subpath, res, hier_prio) + return res + ret = func(d, [], [], 0) + ret = sorted(ret, key=lambda x: x[0]) + ret = sorted(ret, key=lambda x: x[2]) + return ret + +def get_priority_data() -> list: + xml = load_reference() + return priority_data(xml.ref) + +def priority_sort(sections: List[list[str]] = None, + owners: List[str] = None, + reverse=False) -> List: + if sections is not None: + index = 0 + collection: List = sections + elif owners is not None: + index = 1 + collection = owners + else: + raise ValueError('one of sections or owners is required') + + l = get_priority_data() + m = [item for item in l if item[index] in collection] + n = sorted(m, key=lambda x: x[2], reverse=reverse) + o = [item[index] for item in n] + # sections are unhashable; use comprehension + missed = [j for j in collection if j not in o] + if missed: + Warn(f'No priority available for elements {missed}') + + return o diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py index 47318122b..c8e881ee2 100644 --- a/python/vyos/qos/base.py +++ b/python/vyos/qos/base.py @@ -324,6 +324,11 @@ class QoSBase: if 'burst' in cls_config: burst = cls_config['burst'] filter_cmd += f' burst {burst}' + + if 'mtu' in cls_config: + mtu = cls_config['mtu'] + filter_cmd += f' mtu {mtu}' + cls = int(cls) filter_cmd += f' flowid {self._parent:x}:{cls:x}' self._cmd(filter_cmd) @@ -387,6 +392,10 @@ class QoSBase: burst = config['default']['burst'] filter_cmd += f' burst {burst}' + if 'mtu' in config['default']: + mtu = config['default']['mtu'] + filter_cmd += f' mtu {mtu}' + if 'class' in config: filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}' diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py index 37b834ad6..1b487c1d2 100644 --- a/python/vyos/system/compat.py +++ b/python/vyos/system/compat.py @@ -198,11 +198,11 @@ def update_cfg_ver(root_dir:str = '') -> int: return cfg_version -def get_default(menu_entries: list, root_dir: str = '') -> Union[int, None]: +def get_default(data: dict, root_dir: str = '') -> Union[int, None]: """Translate default version to menuentry index Args: - menu_entries (list): list of dicts of installed version boot data + data (dict): boot data root_dir (str): an optional path to the root directory Returns: @@ -213,10 +213,22 @@ def get_default(menu_entries: list, root_dir: str = '') -> Union[int, None]: grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}' + menu_entries = data.get('versions', []) + console_type = data.get('console_type', 'tty') + console_num = data.get('console_num', '0') image_name = image.get_default_image() - sublist = list(filter(lambda x: x.get('version') == image_name, - menu_entries)) + sublist = list(filter(lambda x: (x.get('version') == image_name and + x.get('console_type') == console_type and + x.get('console_num') == console_num and + x.get('bootmode') == 'normal'), + menu_entries)) + # legacy images added with legacy tools omitted 'ttyUSB'; if entry not + # available, default to initial entry of version + if not sublist: + sublist = list(filter(lambda x: x.get('version') == image_name, + menu_entries)) + if sublist: return menu_entries.index(sublist[0]) @@ -291,7 +303,7 @@ def grub_cfg_fields(root_dir: str = '') -> dict: menu_entries = update_version_list(root_dir) fields['versions'] = menu_entries - default = get_default(menu_entries, root_dir) + default = get_default(fields, root_dir) if default is not None: fields['default'] = default diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py index 2e8b20972..e56f0bec8 100644 --- a/python/vyos/system/grub.py +++ b/python/vyos/system/grub.py @@ -17,6 +17,7 @@ import platform from pathlib import Path from re import MULTILINE, compile as re_compile +from shutil import copy2 from typing import Union from uuid import uuid5, NAMESPACE_URL, UUID @@ -373,7 +374,7 @@ def create_structure(root_dir: str = '') -> None: if not root_dir: root_dir = disk.find_persistence() - Path(f'{root_dir}/GRUB_DIR_VYOS_VERS').mkdir(parents=True, exist_ok=True) + Path(f'{root_dir}/{GRUB_DIR_VYOS_VERS}').mkdir(parents=True, exist_ok=True) def set_console_type(console_type: str, root_dir: str = '') -> None: @@ -422,3 +423,38 @@ def set_kernel_cmdline_options(cmdline_options: str, version_name: str, version_add(version_name=version_name, root_dir=root_dir, boot_opts_config=cmdline_options) + + +def sort_inodes(dir_path: str) -> None: + """Sort inodes for files inside a folder + Regenerate inodes for each file to get the same order for both inodes + and file names + + GRUB iterates files by inodes, not alphabetically. Therefore, if we + want to read them in proper order, we need to sort inodes for all + config files in a folder. + + Args: + dir_path (str): a path to directory + """ + dir_content: list[Path] = sorted(Path(dir_path).iterdir()) + temp_list_old: list[Path] = [] + temp_list_new: list[Path] = [] + + # create a copy of all files, to get new inodes + for item in dir_content: + # skip directories + if item.is_dir(): + continue + # create a new copy of file with a temporary name + copy_path = Path(f'{item.as_posix()}_tmp') + copy2(item, Path(copy_path)) + temp_list_old.append(item) + temp_list_new.append(copy_path) + + # delete old files and rename new ones + for item in temp_list_old: + item.unlink() + for item in temp_list_new: + new_name = Path(f'{item.as_posix()[0:-4]}') + item.rename(new_name) diff --git a/python/vyos/template.py b/python/vyos/template.py index 456239568..bde8e3554 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -803,8 +803,8 @@ def kea_address_json(addresses): return dumps(out) -@register_filter('kea_failover_json') -def kea_failover_json(config): +@register_filter('kea_high_availability_json') +def kea_high_availability_json(config): from json import dumps source_addr = config['source_address'] diff --git a/python/vyos/xml_ref/generate_cache.py b/python/vyos/xml_ref/generate_cache.py index 6a05d4608..d1ccb0f81 100755 --- a/python/vyos/xml_ref/generate_cache.py +++ b/python/vyos/xml_ref/generate_cache.py @@ -38,7 +38,8 @@ xml_tmp = join('/tmp', xml_cache_json) pkg_cache = abspath(join(_here, 'pkg_cache')) ref_cache = abspath(join(_here, 'cache.py')) -node_data_fields = ("node_type", "multi", "valueless", "default_value") +node_data_fields = ("node_type", "multi", "valueless", "default_value", + "owner", "priority") def trim_node_data(cache: dict): for k in list(cache): |