summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/component_version.py10
-rw-r--r--python/vyos/config_mgmt.py6
-rw-r--r--python/vyos/configsession.py34
-rw-r--r--python/vyos/kea.py2
-rw-r--r--python/vyos/utils/dict.py53
-rw-r--r--python/vyos/utils/file.py36
-rw-r--r--python/vyos/utils/network.py50
-rw-r--r--python/vyos/vpp/config_resource_checks/memory.py149
-rw-r--r--python/vyos/vpp/config_verify.py86
-rw-r--r--python/vyos/vpp/utils.py11
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