diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/component_version.py | 10 | ||||
| -rw-r--r-- | python/vyos/config_mgmt.py | 6 | ||||
| -rw-r--r-- | python/vyos/configsession.py | 34 | ||||
| -rw-r--r-- | python/vyos/kea.py | 2 | ||||
| -rw-r--r-- | python/vyos/utils/dict.py | 53 | ||||
| -rw-r--r-- | python/vyos/utils/file.py | 36 | ||||
| -rw-r--r-- | python/vyos/utils/network.py | 50 | ||||
| -rw-r--r-- | python/vyos/vpp/config_resource_checks/memory.py | 149 | ||||
| -rw-r--r-- | python/vyos/vpp/config_verify.py | 86 | ||||
| -rw-r--r-- | python/vyos/vpp/utils.py | 11 |
10 files changed, 311 insertions, 126 deletions
diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py index 13fb8333f..136bd36e8 100644 --- a/python/vyos/component_version.py +++ b/python/vyos/component_version.py @@ -209,16 +209,6 @@ def version_info_prune_component(x: VersionInfo, y: VersionInfo) -> VersionInfo: x.component = {k: v for k, v in x.component.items() if k in y.component} -def add_system_version_string(config_str: str = None) -> str: - """Wrap config string with system version and return string.""" - version_info = version_info_from_system() - if config_str is not None: - version_info.update_config_body(config_str) - version_info.update_footer() - - return version_info.write_string() - - def add_system_version(config_str: str = None, out_file: str = None): """Wrap config string with system version and write to out_file. diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index 51c6f2241..b549b3005 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -307,7 +307,11 @@ Proceed ?""" session = ConfigSession(os.getpid(), app='config-mgmt') try: - session.load_explicit(revert_ct) + if session.vyconf_backend(): + session.load_config_obj(revert_ct) + else: + session.load_explicit(revert_ct) + session.commit() except ConfigSessionError as e: raise ConfigMgmtError(e) from e diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 50f93f890..cd0088b60 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -17,6 +17,9 @@ import os import re import sys import subprocess +from tempfile import NamedTemporaryFile +from typing import TypeAlias +from typing import Union from vyos.defaults import directories from vyos.utils.process import is_systemd_service_running @@ -26,6 +29,10 @@ from vyos.utils.backend import vyconf_backend from vyos.vyconf_session import VyconfSession from vyos.base import Warning as Warn from vyos.defaults import DEFAULT_COMMIT_CONFIRM_MINUTES +from vyos.configtree import ConfigTree + +# type of config file path or configtree +ConfigObj: TypeAlias = Union[str, ConfigTree] CLI_SHELL_API = '/bin/cli-shell-api' @@ -238,6 +245,9 @@ class ConfigSession(object): def get_session_env(self): return self.__session_env + def vyconf_backend(self) -> bool: + return bool(self._vyconf_session) + def set(self, path, value=None): if not value: value = [] @@ -309,18 +319,12 @@ class ConfigSession(object): return out def commit_confirm(self, minutes: int = DEFAULT_COMMIT_CONFIRM_MINUTES): - if self._vyconf_session is None: - out = self.__run_command(COMMIT_CONFIRM + [f'-t {minutes}']) - else: - out = 'unimplemented' + out = self.__run_command(COMMIT_CONFIRM + [f'-t {minutes}']) return out def confirm(self): - if self._vyconf_session is None: - out = self.__run_command(CONFIRM) - else: - out = 'unimplemented' + out = self.__run_command(CONFIRM) return out @@ -339,11 +343,13 @@ class ConfigSession(object): if format == 'raw': return config_data - def load_config(self, file_path): + def load_config(self, file_path, cached: bool = False): if self._vyconf_session is None: out = self.__run_command(LOAD_CONFIG + [file_path]) else: - out, _ = self._vyconf_session.load_config(file_name=file_path) + out, _ = self._vyconf_session.load_config( + file_name=file_path, cached=cached + ) return out @@ -356,6 +362,14 @@ class ConfigSession(object): except LoadConfigError as e: raise ConfigSessionError(e) from e + def load_config_obj(self, config_obj: ConfigObj): + if isinstance(config_obj, ConfigTree): + with NamedTemporaryFile() as f: + config_obj.write_cache(f.name) + self.load_config(f.name, cached=True) + else: + self.load_config(config_obj) + def migrate_and_load_config(self, file_path): if self._vyconf_session is None: out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path]) diff --git a/python/vyos/kea.py b/python/vyos/kea.py index a47371e88..c7ae94b14 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -624,9 +624,9 @@ def kea_get_server_leases(config, inet, vrf_name, pools=[], state=[], origin=Non data_lease['origin'] = 'local' # TODO: Determine remote in HA # remove trailing dot in 'hostname' to ensure consistency for `vyos-hostsd-client` data_lease['hostname'] = lease.get('hostname', '').rstrip('.') or '-' + data_lease['mac'] = lease.get('hw-address', '-') if inet == '4': - data_lease['mac'] = lease['hw-address'] data_lease['start'] = lease['start_time'].timestamp() if inet == '6': diff --git a/python/vyos/utils/dict.py b/python/vyos/utils/dict.py index ff21f0677..5d41900b6 100644 --- a/python/vyos/utils/dict.py +++ b/python/vyos/utils/dict.py @@ -145,24 +145,33 @@ def get_sub_dict(source, lpath, get_first_key=False): return ret -def dict_search(path, dict_object): +def dict_search(path, dict_object, default=None): """ Traverse Python dictionary (dict_object) delimited by dot (.). - Return value of key if found, None otherwise. + + Args: + path (str): Dot-delimited key path, e.g. "foo.bar.baz". + dict_object (dict): The dictionary to search. + default (Any, optional): Value to return if the path is not found + or if dict_object is not a dict. Defaults to None. + + Returns: + Any: The value found at the given path, or None if not found. Optionally, + a default value can be provided to be returned. This is faster implementation then jmespath.search('foo.bar', dict_object)""" if not isinstance(dict_object, dict) or not path: - return None + return default parts = path.split('.') inside = parts[:-1] if not inside: if path not in dict_object: - return None + return default return dict_object[path] c = dict_object for p in parts[:-1]: c = c.get(p, {}) - return c.get(parts[-1], None) + return c.get(parts[-1], default) def dict_search_args(dict_object, *path): # Traverse dictionary using variable arguments @@ -211,6 +220,39 @@ def dict_set(key_path, value, dict_object): dynamic_dict = dynamic_dict[path_list[i]] dynamic_dict[path_list[len(path_list)-1]] = value +def dict_set_nested(key_path, value, dict_object): + """ + Set value to Python dictionary (dict_object) using a path to the key + delimited by dot ('.'). The key will be added if it does not exist. + Missing keys along the path will be created as nested dictionaries. + + Parameters + ---------- + key_path : str + Dot-delimited path to the key (e.g. "this.is.a.path"). + value : any + The value to set at the final key in the path. + dict_object : dict + Dictionary to modify. Will be updated in place. + + Examples + -------- + d = {} + dict_set_nested("this.is.a.path", 42, d) + # {'this': {'is': {'a': {'path': 42}}}} + + d = {"existing": {"branch": {}}} + dict_set_nested("existing.branch.leaf", "value", d) + # {'existing': {'branch': {'leaf': 'value'}}} + """ + path_list = key_path.split(".") + dynamic_dict = dict_object + for i in range(0, len(path_list) - 1): + if path_list[i] not in dynamic_dict or not isinstance(dynamic_dict[path_list[i]], dict): + dynamic_dict[path_list[i]] = {} + dynamic_dict = dynamic_dict[path_list[i]] + dynamic_dict[path_list[-1]] = value + def dict_delete(key_path, dict_object): """ Delete key in Python dictionary (dict_object) using path to key delimited by dot (.). """ @@ -370,4 +412,3 @@ class FixedDict(dict): if k not in self._allowed: raise ConfigError(f'Option "{k}" has no defined default') super().__setitem__(k, v) - diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py index c363b5bdc..1e2de2b39 100644 --- a/python/vyos/utils/file.py +++ b/python/vyos/utils/file.py @@ -14,9 +14,6 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os -import tempfile -from contextlib import contextmanager - from vyos.utils.permission import chown def makedir(path, user=None, group=None): @@ -188,36 +185,3 @@ def wait_for_file_write_complete(file_path, pre_hook=None, timeout=None, sleep_i """ Waits for a process to close a file after opening it in write mode. """ wait_for_inotify(file_path, event_type='IN_CLOSE_WRITE', pre_hook=pre_hook, timeout=timeout, sleep_interval=sleep_interval) - -def copy_chown(source, target): - import shutil - import stat - - shutil.copy2(source, target) - st = os.stat(source) - os.chown(target, st[stat.ST_UID], st[stat.ST_GID]) - - -@contextmanager -def write_file_atomic(file_path, mode='w'): - with open(file_path, mode) as _: - pass - temp_file = tempfile.NamedTemporaryFile( - delete=False, dir=os.path.dirname(file_path) - ) - if os.path.exists(file_path): - copy_chown(file_path, temp_file.name) - - file = open(temp_file.name, mode) - try: - yield file - finally: - file.flush() - os.fsync(file.fileno()) - file.close() - os.replace(temp_file.name, file_path) - if os.path.exists(temp_file.name): - try: - os.unlink(temp_file.name) - except Exception: - pass diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 0e2cc58cf..e6b838cdc 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -13,8 +13,10 @@ # 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/>. +import hashlib from socket import AF_INET from socket import AF_INET6 +from vyos.utils.process import cmd def _are_same_ip(one, two): from socket import inet_pton @@ -48,6 +50,49 @@ def is_netns_interface(interface, netns): return True return False +def get_host_identity() -> str: + """ + Build a stable host identity string for deterministic MAC generation. + + Combines: + • The system's HardwareUUID (from /sys/class/dmi/id/product_uuid) + • The system hostname + + Both are normalized (lowercase, dashes removed in UUID) and joined with a colon. + + Returns: + str: A string "<uuid>:<hostname>", used as part of the host-specific seed when + generating deterministic MAC addresses. + """ + uuid = cmd(f"cat /sys/class/dmi/id/product_uuid").strip().replace("-", "").lower() + host = cmd("hostname").strip().lower() + return f"{uuid}:{host}" + +def gen_mac(name: str, addr: str, ident: str) -> str: + """ + Generate a deterministic locally-administered MAC address. + + The MAC is derived from: + • Host identity (UUID + hostname) + • Container name + • Concatenated address string (IPv4 and/or IPv6 addresses) + + A SHA-256 digest is computed from the combined string. The first 5 bytes + of the digest are used, prefixed with 0x02 to mark the address as + locally-administered and unicast. + + Args: + name (str): Container name to differentiate MACs. + addr (str): Concatenated list of container addresses (IPv4/IPv6). + + Returns: + str: Deterministic MAC address in standard "xx:xx:xx:xx:xx:xx" format. + """ + h = hashlib.sha256(f"{ident}:{name}:{addr}".encode()).hexdigest() + # 0x02 = locally-administered, unicast + b = [0x02] + [int(h[i:i+2], 16) for i in range(0, 10, 2)] # 5 bytes = 40 bits + return ":".join(f"{x:02x}" for x in b) + def get_netns_all() -> list: from json import loads from vyos.utils.process import cmd @@ -81,7 +126,10 @@ def get_interface_vrf(interface): """ Returns VRF of given interface """ from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config - tmp = get_interface_config(interface) + if isinstance(interface, str): + tmp = get_interface_config(interface) + elif isinstance(interface, dict): + tmp = interface if dict_search('linkinfo.info_slave_kind', tmp) == 'vrf': return tmp['master'] return 'default' diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py index 66b9fd9a3..ea4697ba2 100644 --- a/python/vyos/vpp/config_resource_checks/memory.py +++ b/python/vyos/vpp/config_resource_checks/memory.py @@ -16,7 +16,9 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import os import re +import psutil from vyos.utils.process import cmd from vyos.vpp.utils import ( @@ -26,29 +28,59 @@ from vyos.vpp.utils import ( from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map +# VPP buffers default per NUMA node +MIN_BUFFERS = 16_384 + + +def classify_page_size(page_size_bytes: int) -> str: + """ + Returns one of: '4K', '2M', '1G' based on page size. + """ + if page_size_bytes == 1 << 30: + return '1G' + if page_size_bytes == 2 << 20: + return '2M' + return '4K' + + def get_hugepages_info() -> dict: """ - Returns the information about HugePages for default hugepage size - retrieved from /proc/meminfo + Returns the information about HugePages + retrieved from /sys/kernel/mm/hugepages """ + base_path = '/sys/kernel/mm/hugepages' info = {} - with open('/proc/meminfo', 'r') as meminfo: - for line in meminfo: - if line.startswith('Huge'): - key, value, *_ = line.strip().split() - info[key.rstrip(':')] = int(value) + + for entry in os.listdir(base_path): + page_size_kb = entry[10:] + page_size = human_page_memory_to_bytes(page_size_kb) + key = classify_page_size(page_size) + info[key] = {} + + with open(os.path.join(base_path, entry, 'nr_hugepages')) as f: + count = int(f.read().strip()) + info[key]['pages'] = count + info[key]['memory'] = page_size * count + return info -def get_total_hugepages_memory() -> int: +def get_available_memory() -> dict: + memory = {size: info.get('memory') for size, info in get_hugepages_info().items()} + memory['4K'] = psutil.virtual_memory().available + + return memory + + +def get_vpp_used_memory() -> int: """ - Returns the total amount of hugepage memory (in bytes) + Returns memory currently used by VPP in bytes (RSS value) """ - info = get_hugepages_info() - hugepages_total = info.get('HugePages_Total') - hugepage_size = info.get('Hugepagesize') * 1024 - - return hugepage_size * hugepages_total + try: + out = cmd('ps -o rss= -p $(pidof vpp)') + except OSError: + out = 0 + return int(out) << 10 def get_numa_count(): @@ -61,6 +93,11 @@ def get_numa_count(): return int(m.group(1)) if m else 0 +def buffer_page_size(settings: dict) -> int: + page_size = settings.get('buffers', {}).get('page_size', 'default') + return human_page_memory_to_bytes(page_size) + + def buffer_size(settings: dict) -> int: numa_count = get_numa_count() buffers_per_numa = int( @@ -110,7 +147,7 @@ def statseg_size(settings: dict) -> int: def statseg_page_size(settings: dict) -> int: - page_size = settings.get('statseg', {}).get('page_size', 'default') + page_size = settings.get('statseg', {}).get('page_size') return human_page_memory_to_bytes(page_size) @@ -118,28 +155,76 @@ def total_statseg_size(_statseg_size: int, _statseg_page: int) -> int: return (_statseg_size + _statseg_page - 1) & ~(_statseg_page - 1) -def total_memory_required(settings: dict) -> int: - mem_required = 0 +def total_memory_required(settings: dict) -> dict: + memory = {'2M': 0, '1G': 0, '4K': 0} mem_stats = { - 'memory_buffers': buffer_size(settings), - 'netlink_buffer_size': int( - settings.get('lcp', {}).get( - 'rx_buffer_size', default_resource_map.get('netlink_rx_buffer_size') - ) + 'memory_buffers': (buffer_size(settings), buffer_page_size(settings)), + 'netlink_buffer_size': ( + int( + settings.get('lcp', {}) + .get('netlink', {}) + .get( + 'rx_buffer_size', default_resource_map.get('netlink_rx_buffer_size') + ) + ), + 0, ), - 'heap_size': total_heap_size( - heap_size=memory_main_heap(settings), - heap_page_size=main_heap_page_size(settings), + 'heap_size': ( + total_heap_size( + heap_size=memory_main_heap(settings), + heap_page_size=main_heap_page_size(settings), + ), + main_heap_page_size(settings), ), - 'statseg_size': total_statseg_size( - _statseg_size=statseg_size(settings), - _statseg_page=statseg_page_size(settings), + 'statseg_size': ( + total_statseg_size( + _statseg_size=statseg_size(settings), + _statseg_page=statseg_page_size(settings), + ), + statseg_page_size(settings), ), - 'ipv6_heap_size': ipv6_heap_size(settings), + 'ipv6_heap_size': (ipv6_heap_size(settings), 0), } - for stat in mem_stats: - mem_required += mem_stats[stat] + for memory_size, page_size in mem_stats.values(): + memory[classify_page_size(page_size)] += memory_size + + return memory + - return mem_required +def buffers_required(settings: dict, workers) -> int: + """ + Calculate total VPP buffer requirements based on interface settings and workers. + """ + buffers_total = 0 + for ifname, iface_config in settings.get('interface', {}).items(): + # Do not include XDP interfaces in buffer calculations. + # Unlike DPDK, XDP does not use VPP-managed mbufs for RX/TX rings, + # so buffer requirements cannot be derived from descriptors here. + # Buffers for XDP are handled internally by the kernel/XDP layer, + # not by VPP’s buffer allocator. + if iface_config.get('driver') == 'xdp': + continue + dpdk_options = iface_config.get('dpdk_options', {}) + rx_queues = int(dpdk_options.get('num_rx_queues', 1)) + rx_desc = int(dpdk_options.get('num_rx_desc')) + # default TX queues is equal to number of worker threads + # plus 1 main thread + tx_queues = int(dpdk_options.get('num_tx_queues', workers + 1)) + tx_desc = int(dpdk_options.get('num_tx_desc')) + + # buffers for RX/TX queues for interface + buffers_total += rx_queues * rx_desc + tx_queues * tx_desc + + # per-thread buffer caches (approx. 256 buffers per worker) + buffers_total += workers * 256 + + # Safety margin for buffer calculations: + # traffic bursts, alignment/metadata overhead etc. + # The factor 2.5 is derived from VPP’s own calculations: + # https://github.com/FDio/vpp/blob/stable/2506/extras/vpp_config/vpplib/AutoConfig.py#L609 + buffers_total = int(buffers_total * 2.5) + + # Enforce minimum required by VPP (16K buffers) + return max(buffers_total, MIN_BUFFERS) diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py index c6f43e340..71cf97423 100644 --- a/python/vyos/vpp/config_verify.py +++ b/python/vyos/vpp/config_verify.py @@ -211,9 +211,9 @@ def verify_vpp_minimum_memory(): ) -def verify_vpp_memory(config: dict): - main_heap_size = mem_checks.memory_main_heap(config['settings']) - main_heap_page_size = mem_checks.main_heap_page_size(config['settings']) +def verify_vpp_main_heap_size(settings: dict): + main_heap_size = mem_checks.memory_main_heap(settings) + main_heap_page_size = mem_checks.main_heap_page_size(settings) if main_heap_size < 1 << 30: raise ConfigError('The main heap size must be greater than or equal to 1G') @@ -225,28 +225,54 @@ def verify_vpp_memory(config: dict): f'The main heap size must be greater than or equal to page-size ({readable_heap_page})' ) - available_memory = mem_checks.get_total_hugepages_memory() - memory_required = mem_checks.total_memory_required(config['settings']) - if main_heap_size > available_memory: - available_memory_in_mb = bytes_to_human_memory(available_memory, 'M') - raise ConfigError( - f'"memory main-heap-size" must not be greater than hugepages memory. Reduce to {available_memory_in_mb} or less' +def verify_vpp_memory(config: dict): + memory_required = mem_checks.total_memory_required(config['settings']) + memory_available = mem_checks.get_available_memory() + + # Check if there is a config currently active + # If yes, calculate how much memory it consumes (only for 4k pages) + # and exclude it from required memory + if config.get('effective'): + memory_effective = mem_checks.total_memory_required( + config['effective']['settings'] ) - memory_required = round(memory_required / 1024**3, 1) - available_memory = round(available_memory / 1024**3, 1) - - # Allow 10% error margin - allowed_margin = memory_required * 0.1 + # If we want to reduce memory configs then we don't need + # to check 4K memory type + if memory_effective['4K'] >= memory_required['4K']: + del memory_required['4K'] + else: + # Get memory currently used by VPP and add it to available memory + memory_used = mem_checks.get_vpp_used_memory() + memory_available['4K'] += memory_used + + memory_required_gb = {k: round(v / 1024**3, 1) for k, v in memory_required.items()} + memory_available_gb = { + k: round(v / 1024**3, 1) for k, v in memory_available.items() + } + + errors = {} + for page_size, req_gb in memory_required_gb.items(): + avail_gb = memory_available_gb.get(page_size, 0) + + if req_gb > avail_gb: + label = 'System' if page_size == '4K' else f'{page_size} HugePages' + errors[page_size] = ( + f'{label} memory: available {avail_gb} GB, ' + f'required {memory_required_gb[page_size]} GB' + ) - # Compare HugePage memory with required memory for VPP - if memory_required > available_memory + allowed_margin: + if errors: raise ConfigError( - f'Not enough free hugepage memory to start VPP: ' - f'available: {available_memory} GB, required: {memory_required} GB. ' - 'Please add kernel memory options for HugePages ' - '"set system option kernel memory hugepage-size ..." and reboot' + 'Not enough free memory to start VPP! '.ljust(72) + + '. '.join([line.ljust(72) for line in errors.values()]) + + ( + 'To add HugePages memory please use command '.ljust(72) + + '"set system option kernel memory hugepage-size ..." and reboot!' + if any(k in errors for k in ('2M', '1G')) + else '' + ) ) @@ -291,7 +317,7 @@ def verify_vpp_cpu_main_core(cpu_settings: dict) -> None: ) -def verify_vpp_settings_cpu_workers(cpu_settings: dict) -> int: +def verify_vpp_settings_cpu_workers(cpu_settings: dict): """ Verify that the system has enough available CPU cores to run a given amount of worker processes (1 worker/core) @@ -305,10 +331,8 @@ def verify_vpp_settings_cpu_workers(cpu_settings: dict) -> int: f'(reduce to {available_cores} or less)' ) - return workers - -def verify_vpp_settings_cpu_corelist_workers(cpu_settings: dict) -> int: +def verify_vpp_settings_cpu_corelist_workers(cpu_settings: dict): """ Verify that the CPU cores provided to the config are free and can be used by VPP """ @@ -340,8 +364,6 @@ def verify_vpp_settings_cpu_corelist_workers(cpu_settings: dict) -> int: if len(all_core_nums) > cpu_checks.available_cores_count(cpu_settings): raise ConfigError(f'{error_msg}: Not enough free CPUs in the system.') - return len(all_core_nums) - def verify_vpp_nat44_workers(workers: int, nat44_workers: list): if workers < 1: @@ -416,3 +438,15 @@ def verify_routes_count(settings: dict, workers: int): 'Extensive use of features like ACLs, NAT and others may reduce the numbers above. ' 'Please read the documentation for details: https://docs.vyos.io/' ) + + +def verify_vpp_buffers(settings: dict, workers: int): + buffers_configured = int(settings['buffers']['buffers_per_numa']) + + buffers_required = mem_checks.buffers_required(settings, workers) + + if buffers_required > buffers_configured: + raise ConfigError( + 'Not enough buffers to initialize RX/TX queues for interfaces. ' + f'Set "vpp settings buffers buffers-per-numa" to {buffers_required} or higher' + ) diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py index 8b3a8c038..4e42232cd 100644 --- a/python/vyos/vpp/utils.py +++ b/python/vyos/vpp/utils.py @@ -23,7 +23,7 @@ from pathlib import Path from struct import pack -mem_shift = {'K': 10, 'k': 10, 'M': 20, 'm': 20, 'G': 30, 'g': 30} +mem_shift = {'K': 10, 'KB': 10, 'M': 20, 'MB': 20, 'G': 30, 'GB': 30} def iftunnel_transform(iface: str) -> str: @@ -302,15 +302,19 @@ def get_hugepage_sizes() -> list[int]: def human_memory_to_bytes(value: str) -> int: """ - Convert a human-readable vpp memory format (K, M, G) to a byte value. + Convert a human-readable vpp memory format (K, M, G, xB) to a byte value. :param value: The string memory size in vpp human-readable format. :return: A int representing the value. """ + value = value.strip().upper() try: return int(value) except ValueError: - return int(value[:-1]) << mem_shift[value[-1]] + for unit in sorted(mem_shift.keys(), key=len, reverse=True): + if value.endswith(unit): + num = value[: -len(unit)] + return int(num) << mem_shift[unit] def bytes_to_human_memory(value: int, unit: str) -> str | None: @@ -321,6 +325,7 @@ def bytes_to_human_memory(value: int, unit: str) -> str | None: :param unit: The unit to convert to ('K', 'M', 'G'). :return: A string representing the value in the specified unit, or None if zero. """ + unit = unit.upper() val = value >> mem_shift[unit] return f'{val}{unit}' if val else None |
