summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/configdict.py78
-rw-r--r--python/vyos/ethtool.py3
-rw-r--r--python/vyos/ifconfig/geneve.py1
-rw-r--r--python/vyos/qos/base.py104
-rw-r--r--python/vyos/qos/limiter.py1
-rw-r--r--python/vyos/qos/trafficshaper.py1
-rw-r--r--python/vyos/utils/system.py82
-rw-r--r--python/vyos/vpp.py315
-rw-r--r--python/vyos/xml_ref/definition.py4
9 files changed, 465 insertions, 124 deletions
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 9618ec93e..1205342df 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -595,40 +595,8 @@ def get_accel_dict(config, base, chap_secrets):
dict = config.get_config_dict(base, key_mangling=('-', '_'),
get_first_key=True,
- no_tag_node_value_mangle=True)
-
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- default_values = defaults(base)
-
- # T2665: defaults include RADIUS server specifics per TAG node which need to
- # be added to individual RADIUS servers instead - so we can simply delete them
- if dict_search('authentication.radius.server', default_values):
- del default_values['authentication']['radius']['server']
-
- # T2665: defaults include static-ip address per TAG node which need to be
- # added to individual local users instead - so we can simply delete them
- if dict_search('authentication.local_users.username', default_values):
- del default_values['authentication']['local_users']['username']
-
- # T2665: defaults include IPv6 client-pool mask per TAG node which need to be
- # added to individual local users instead - so we can simply delete them
- if dict_search('client_ipv6_pool.prefix.mask', default_values):
- del default_values['client_ipv6_pool']['prefix']['mask']
- # delete empty dicts
- if len (default_values['client_ipv6_pool']['prefix']) == 0:
- del default_values['client_ipv6_pool']['prefix']
- if len (default_values['client_ipv6_pool']) == 0:
- del default_values['client_ipv6_pool']
-
- # T2665: IPoE only - it has an interface tag node
- # added to individual local users instead - so we can simply delete them
- if dict_search('authentication.interface', default_values):
- del default_values['authentication']['interface']
- if dict_search('interface', default_values):
- del default_values['interface']
-
- dict = dict_merge(default_values, dict)
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True)
# set CPUs cores to process requests
dict.update({'thread_count' : get_half_cpus()})
@@ -648,43 +616,9 @@ def get_accel_dict(config, base, chap_secrets):
dict.update({'name_server_ipv4' : ns_v4, 'name_server_ipv6' : ns_v6})
del dict['name_server']
- # T2665: Add individual RADIUS server default values
- if dict_search('authentication.radius.server', dict):
- default_values = defaults(base + ['authentication', 'radius', 'server'])
- for server in dict_search('authentication.radius.server', dict):
- dict['authentication']['radius']['server'][server] = dict_merge(
- default_values, dict['authentication']['radius']['server'][server])
-
- # Check option "disable-accounting" per server and replace default value from '1813' to '0'
- # set vpn sstp authentication radius server x.x.x.x disable-accounting
- if 'disable_accounting' in dict['authentication']['radius']['server'][server]:
- dict['authentication']['radius']['server'][server]['acct_port'] = '0'
-
- # T2665: Add individual local-user default values
- if dict_search('authentication.local_users.username', dict):
- default_values = defaults(base + ['authentication', 'local-users', 'username'])
- for username in dict_search('authentication.local_users.username', dict):
- dict['authentication']['local_users']['username'][username] = dict_merge(
- default_values, dict['authentication']['local_users']['username'][username])
-
- # T2665: Add individual IPv6 client-pool default mask if required
- if dict_search('client_ipv6_pool.prefix', dict):
- default_values = defaults(base + ['client-ipv6-pool', 'prefix'])
- for prefix in dict_search('client_ipv6_pool.prefix', dict):
- dict['client_ipv6_pool']['prefix'][prefix] = dict_merge(
- default_values, dict['client_ipv6_pool']['prefix'][prefix])
-
- # T2665: IPoE only - add individual local-user default values
- if dict_search('authentication.interface', dict):
- default_values = defaults(base + ['authentication', 'interface'])
- for interface in dict_search('authentication.interface', dict):
- dict['authentication']['interface'][interface] = dict_merge(
- default_values, dict['authentication']['interface'][interface])
-
- if dict_search('interface', dict):
- default_values = defaults(base + ['interface'])
- for interface in dict_search('interface', dict):
- dict['interface'][interface] = dict_merge(default_values,
- dict['interface'][interface])
+ # Check option "disable-accounting" per server and replace default value from '1813' to '0'
+ for server in (dict_search('authentication.radius.server', dict) or []):
+ if 'disable_accounting' in dict['authentication']['radius']['server'][server]:
+ dict['authentication']['radius']['server'][server]['acct_port'] = '0'
return dict
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index 68234089c..9b7da89fa 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -21,7 +21,8 @@ from vyos.util import popen
# These drivers do not support using ethtool to change the speed, duplex, or
# flow control settings
_drivers_without_speed_duplex_flow = ['vmxnet3', 'virtio_net', 'xen_netfront',
- 'iavf', 'ice', 'i40e', 'hv_netvsc', 'veth', 'ixgbevf']
+ 'iavf', 'ice', 'i40e', 'hv_netvsc', 'veth', 'ixgbevf',
+ 'tun']
class Ethtool:
"""
diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py
index 276c34cd7..7a05e47a7 100644
--- a/python/vyos/ifconfig/geneve.py
+++ b/python/vyos/ifconfig/geneve.py
@@ -45,6 +45,7 @@ class GeneveIf(Interface):
'parameters.ip.df' : 'df',
'parameters.ip.tos' : 'tos',
'parameters.ip.ttl' : 'ttl',
+ 'parameters.ip.innerproto' : 'innerprotoinherit',
'parameters.ipv6.flowlabel' : 'flowlabel',
}
diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py
index 26ec65535..3983b1bc0 100644
--- a/python/vyos/qos/base.py
+++ b/python/vyos/qos/base.py
@@ -61,6 +61,7 @@ class QoSBase:
"CS7": 0xE0,
"EF": 0xB8
}
+ qostype = None
def __init__(self, interface):
if os.path.exists('/tmp/vyos.qos.debug'):
@@ -203,18 +204,21 @@ class QoSBase:
self._build_base_qdisc(cls_config, int(cls))
# every match criteria has it's tc instance
- filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}:'
+ filter_cmd_base = f'tc filter add dev {self._interface} parent {self._parent:x}:'
if priority:
- filter_cmd += f' prio {cls}'
+ filter_cmd_base += f' prio {cls}'
elif 'priority' in cls_config:
prio = cls_config['priority']
- filter_cmd += f' prio {prio}'
+ filter_cmd_base += f' prio {prio}'
- filter_cmd += ' protocol all'
+ filter_cmd_base += ' protocol all'
if 'match' in cls_config:
- for match, match_config in cls_config['match'].items():
+ for index, (match, match_config) in enumerate(cls_config['match'].items(), start=1):
+ filter_cmd = filter_cmd_base
+ if self.qostype == 'shaper' and 'prio ' not in filter_cmd:
+ filter_cmd += f' prio {index}'
if 'mark' in match_config:
mark = match_config['mark']
filter_cmd += f' handle {mark} fw'
@@ -289,10 +293,19 @@ class QoSBase:
elif af == 'ipv6':
filter_cmd += f' match u8 {mask} {mask} at 53'
+ cls = int(cls)
+ filter_cmd += f' flowid {self._parent:x}:{cls:x}'
+ self._cmd(filter_cmd)
+
else:
filter_cmd += ' basic'
+ cls = int(cls)
+ filter_cmd += f' flowid {self._parent:x}:{cls:x}'
+ self._cmd(filter_cmd)
+
+
# The police block allows limiting of the byte or packet rate of
# traffic matched by the filter it is attached to.
# https://man7.org/linux/man-pages/man8/tc-police.8.html
@@ -318,48 +331,41 @@ class QoSBase:
# burst = cls_config['burst']
# filter_cmd += f' burst {burst}'
- cls = int(cls)
- filter_cmd += f' flowid {self._parent:x}:{cls:x}'
- self._cmd(filter_cmd)
+ if 'default' in config:
+ default_cls_id = 1
+ if 'class' in config:
+ class_id_max = self._get_class_max_id(config)
+ default_cls_id = int(class_id_max) +1
+ self._build_base_qdisc(config['default'], default_cls_id)
+
+ if self.qostype == 'limiter':
+ if 'default' in config:
+ filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}: '
+ filter_cmd += 'prio 255 protocol all basic'
+
+ # The police block allows limiting of the byte or packet rate of
+ # traffic matched by the filter it is attached to.
+ # https://man7.org/linux/man-pages/man8/tc-police.8.html
+ if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in
+ config['default']):
+ filter_cmd += f' action police'
+
+ if 'exceed' in config['default']:
+ action = config['default']['exceed']
+ filter_cmd += f' conform-exceed {action}'
+ if 'not_exceed' in config['default']:
+ action = config['default']['not_exceed']
+ filter_cmd += f'/{action}'
+
+ if 'bandwidth' in config['default']:
+ rate = self._rate_convert(config['default']['bandwidth'])
+ filter_cmd += f' rate {rate}'
- # T5295: Do not do any tc filter action for 'default'
- # In VyOS 1.4, we have the following configuration:
- # tc filter replace dev eth0 parent 1: prio 255 protocol all basic action police rate 300000000 burst 15k
- # However, this caused unexpected random speeds.
- # In VyOS 1.3, we do not use any 'tc filter' for rate limits,
- # It gets rate from tc class classid 1:1
- #
- # if 'default' in config:
- # if 'class' in config:
- # class_id_max = self._get_class_max_id(config)
- # default_cls_id = int(class_id_max) +1
- # self._build_base_qdisc(config['default'], default_cls_id)
- #
- # filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}: '
- # filter_cmd += 'prio 255 protocol all basic'
- #
- # # The police block allows limiting of the byte or packet rate of
- # # traffic matched by the filter it is attached to.
- # # https://man7.org/linux/man-pages/man8/tc-police.8.html
- # if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in config['default']):
- # filter_cmd += f' action police'
- #
- # if 'exceed' in config['default']:
- # action = config['default']['exceed']
- # filter_cmd += f' conform-exceed {action}'
- # if 'not_exceed' in config['default']:
- # action = config['default']['not_exceed']
- # filter_cmd += f'/{action}'
- #
- # if 'bandwidth' in config['default']:
- # rate = self._rate_convert(config['default']['bandwidth'])
- # filter_cmd += f' rate {rate}'
- #
- # if 'burst' in config['default']:
- # burst = config['default']['burst']
- # filter_cmd += f' burst {burst}'
- #
- # if 'class' in config:
- # filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}'
- #
- # self._cmd(filter_cmd)
+ if 'burst' in config['default']:
+ burst = config['default']['burst']
+ filter_cmd += f' burst {burst}'
+
+ if 'class' in config:
+ filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}'
+
+ self._cmd(filter_cmd)
diff --git a/python/vyos/qos/limiter.py b/python/vyos/qos/limiter.py
index ace0c0b6c..3f5c11112 100644
--- a/python/vyos/qos/limiter.py
+++ b/python/vyos/qos/limiter.py
@@ -17,6 +17,7 @@ from vyos.qos.base import QoSBase
class Limiter(QoSBase):
_direction = ['ingress']
+ qostype = 'limiter'
def update(self, config, direction):
tmp = f'tc qdisc add dev {self._interface} handle {self._parent:x}: {direction}'
diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py
index 573283833..c63c7cf39 100644
--- a/python/vyos/qos/trafficshaper.py
+++ b/python/vyos/qos/trafficshaper.py
@@ -22,6 +22,7 @@ MINQUANTUM = 1000
class TrafficShaper(QoSBase):
_parent = 1
+ qostype = 'shaper'
# https://man7.org/linux/man-pages/man8/tc-htb.8.html
def update(self, config, direction):
diff --git a/python/vyos/utils/system.py b/python/vyos/utils/system.py
new file mode 100644
index 000000000..7102d5985
--- /dev/null
+++ b/python/vyos/utils/system.py
@@ -0,0 +1,82 @@
+# Copyright 2023 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 subprocess import run
+
+
+def sysctl_read(name: str) -> str:
+ """Read and return current value of sysctl() option
+
+ Args:
+ name (str): sysctl key name
+
+ Returns:
+ str: sysctl key value
+ """
+ tmp = run(['sysctl', '-nb', name], capture_output=True)
+ return tmp.stdout.decode()
+
+
+def sysctl_write(name: str, value: str | int) -> bool:
+ """Change value via sysctl()
+
+ Args:
+ name (str): sysctl key name
+ value (str | int): sysctl key value
+
+ Returns:
+ bool: True if changed, False otherwise
+ """
+ # convert other types to string before comparison
+ if not isinstance(value, str):
+ value = str(value)
+ # do not change anything if a value is already configured
+ if sysctl_read(name) == value:
+ return True
+ # return False if sysctl call failed
+ if run(['sysctl', '-wq', f'{name}={value}']).returncode != 0:
+ return False
+ # compare old and new values
+ # sysctl may apply value, but its actual value will be
+ # different from requested
+ if sysctl_read(name) == value:
+ return True
+ # False in other cases
+ return False
+
+
+def sysctl_apply(sysctl_dict: dict[str, str], revert: bool = True) -> bool:
+ """Apply sysctl values.
+
+ Args:
+ sysctl_dict (dict[str, str]): dictionary with sysctl keys with values
+ revert (bool, optional): Revert to original values if new were not
+ applied. Defaults to True.
+
+ Returns:
+ bool: True if all params configured properly, False in other cases
+ """
+ # get current values
+ sysctl_original: dict[str, str] = {}
+ for key_name in sysctl_dict.keys():
+ sysctl_original[key_name] = sysctl_read(key_name)
+ # apply new values and revert in case one of them was not applied
+ for key_name, value in sysctl_dict.items():
+ if not sysctl_write(key_name, value):
+ if revert:
+ sysctl_apply(sysctl_original, revert=False)
+ return False
+ # everything applied
+ return True
diff --git a/python/vyos/vpp.py b/python/vyos/vpp.py
new file mode 100644
index 000000000..76e5d29c3
--- /dev/null
+++ b/python/vyos/vpp.py
@@ -0,0 +1,315 @@
+# Copyright 2023 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 functools import wraps
+from pathlib import Path
+from re import search as re_search, fullmatch as re_fullmatch, MULTILINE as re_M
+from subprocess import run
+from time import sleep
+
+from vpp_papi import VPPApiClient
+from vpp_papi import VPPIOError, VPPValueError
+
+
+class VPPControl:
+ """Control VPP network stack
+ """
+
+ class _Decorators:
+ """Decorators for VPPControl
+ """
+
+ @classmethod
+ def api_call(cls, decorated_func):
+ """Check if API is connected before API call
+
+ Args:
+ decorated_func: function to decorate
+
+ Raises:
+ VPPIOError: Connection to API is not established
+ """
+
+ @wraps(decorated_func)
+ def api_safe_wrapper(cls, *args, **kwargs):
+ if not cls.vpp_api_client.transport.connected:
+ raise VPPIOError(2, 'VPP API is not connected')
+ return decorated_func(cls, *args, **kwargs)
+
+ return api_safe_wrapper
+
+ @classmethod
+ def check_retval(cls, decorated_func):
+ """Check retval from API response
+
+ Args:
+ decorated_func: function to decorate
+
+ Raises:
+ VPPValueError: raised when retval is not 0
+ """
+
+ @wraps(decorated_func)
+ def check_retval_wrapper(cls, *args, **kwargs):
+ return_value = decorated_func(cls, *args, **kwargs)
+ if not return_value.retval == 0:
+ raise VPPValueError(
+ f'VPP API call failed: {return_value.retval}')
+ return return_value
+
+ return check_retval_wrapper
+
+ def __init__(self, attempts: int = 5, interval: int = 1000) -> None:
+ """Create VPP API connection
+
+ Args:
+ attempts (int, optional): attempts to connect. Defaults to 5.
+ interval (int, optional): interval between attempts in ms. Defaults to 1000.
+
+ Raises:
+ VPPIOError: Connection to API cannot be established
+ """
+ self.vpp_api_client = VPPApiClient()
+ # connect with interval
+ while attempts:
+ try:
+ attempts -= 1
+ self.vpp_api_client.connect('vpp-vyos')
+ break
+ except (ConnectionRefusedError, FileNotFoundError) as err:
+ print(f'VPP API connection timeout: {err}')
+ sleep(interval / 1000)
+ # raise exception if connection was not successful in the end
+ if not self.vpp_api_client.transport.connected:
+ raise VPPIOError(2, 'Cannot connect to VPP API')
+
+ def __del__(self) -> None:
+ """Disconnect from VPP API (destructor)
+ """
+ self.disconnect()
+
+ def disconnect(self) -> None:
+ """Disconnect from VPP API
+ """
+ if self.vpp_api_client.transport.connected:
+ self.vpp_api_client.disconnect()
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def cli_cmd(self, command: str):
+ """Send raw CLI command
+
+ Args:
+ command (str): command to send
+
+ Returns:
+ vpp_papi.vpp_serializer.cli_inband_reply: CLI reply class
+ """
+ return self.vpp_api_client.api.cli_inband(cmd=command)
+
+ @_Decorators.api_call
+ def get_mac(self, ifname: str) -> str:
+ """Find MAC address by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ str: MAC address
+ """
+ for iface in self.vpp_api_client.api.sw_interface_dump():
+ if iface.interface_name == ifname:
+ return iface.l2_address.mac_string
+ return ''
+
+ @_Decorators.api_call
+ def get_sw_if_index(self, ifname: str) -> int | None:
+ """Find interface index by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ int | None: Interface index or None (if was not fount)
+ """
+ for iface in self.vpp_api_client.api.sw_interface_dump():
+ if iface.interface_name == ifname:
+ return iface.sw_if_index
+ return None
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def lcp_pair_add(self, iface_name_vpp: str, iface_name_kernel: str) -> None:
+ """Create LCP interface pair between VPP and kernel
+
+ Args:
+ iface_name_vpp (str): interface name in VPP
+ iface_name_kernel (str): interface name in kernel
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ return self.vpp_api_client.api.lcp_itf_pair_add_del(
+ is_add=True,
+ sw_if_index=iface_index,
+ host_if_name=iface_name_kernel)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def lcp_pair_del(self, iface_name_vpp: str, iface_name_kernel: str) -> None:
+ """Delete LCP interface pair between VPP and kernel
+
+ Args:
+ iface_name_vpp (str): interface name in VPP
+ iface_name_kernel (str): interface name in kernel
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ return self.vpp_api_client.api.lcp_itf_pair_add_del(
+ is_add=False,
+ sw_if_index=iface_index,
+ host_if_name=iface_name_kernel)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def iface_rxmode(self, iface_name: str, rx_mode: str) -> None:
+ """Set interface rx-mode in VPP
+
+ Args:
+ iface_name (str): interface name in VPP
+ rx_mode (str): mode (polling, interrupt, adaptive)
+ """
+ modes_dict: dict[str, int] = {
+ 'polling': 1,
+ 'interrupt': 2,
+ 'adaptive': 3
+ }
+ if rx_mode not in modes_dict:
+ raise VPPValueError(f'Mode {rx_mode} is not known')
+ iface_index = self.get_sw_if_index(iface_name)
+ return self.vpp_api_client.api.sw_interface_set_rx_mode(
+ sw_if_index=iface_index, mode=modes_dict[rx_mode])
+
+ @_Decorators.api_call
+ def get_pci_addr(self, ifname: str) -> str:
+ """Find PCI address of interface by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ str: PCI address
+ """
+ hw_info = self.cli_cmd(f'show hardware-interfaces {ifname}').reply
+
+ regex_filter = r'^\s+pci: device (?P<device>\w+:\w+) subsystem (?P<subsystem>\w+:\w+) address (?P<address>\w+:\w+:\w+\.\w+) numa (?P<numa>\w+)$'
+ re_obj = re_search(regex_filter, hw_info, re_M)
+
+ # return empty string if no interface or no PCI info was found
+ if not hw_info or not re_obj:
+ return ''
+
+ address = re_obj.groupdict().get('address', '')
+
+ # we need to modify address to math kernel style
+ # for example: 0000:06:14.00 -> 0000:06:14.0
+ address_chunks: list[str] = address.split('.')
+ address_normalized: str = f'{address_chunks[0]}.{int(address_chunks[1])}'
+
+ return address_normalized
+
+
+class HostControl:
+ """Control Linux host
+ """
+
+ @staticmethod
+ def pci_rescan(pci_addr: str = '') -> None:
+ """Rescan PCI device by removing it and rescan PCI bus
+
+ If PCI address is not defined - just rescan PCI bus
+
+ Args:
+ address (str, optional): PCI address of device. Defaults to ''.
+ """
+ if pci_addr:
+ device_file = Path(f'/sys/bus/pci/devices/{pci_addr}/remove')
+ if device_file.exists():
+ device_file.write_text('1')
+ # wait 10 seconds max until device will be removed
+ attempts = 100
+ while device_file.exists() and attempts:
+ attempts -= 1
+ sleep(0.1)
+ if device_file.exists():
+ raise TimeoutError(
+ f'Timeout was reached for removing PCI device {pci_addr}'
+ )
+ else:
+ raise FileNotFoundError(f'PCI device {pci_addr} does not exist')
+ rescan_file = Path('/sys/bus/pci/rescan')
+ rescan_file.write_text('1')
+ if pci_addr:
+ # wait 10 seconds max until device will be installed
+ attempts = 100
+ while not device_file.exists() and attempts:
+ attempts -= 1
+ sleep(0.1)
+ if not device_file.exists():
+ raise TimeoutError(
+ f'Timeout was reached for installing PCI device {pci_addr}')
+
+ @staticmethod
+ def get_eth_name(pci_addr: str) -> str:
+ """Find Ethernet interface name by PCI address
+
+ Args:
+ pci_addr (str): PCI address
+
+ Raises:
+ FileNotFoundError: no Ethernet interface was found
+
+ Returns:
+ str: Ethernet interface name
+ """
+ # find all PCI devices with eth* names
+ net_devs: dict[str, str] = {}
+ net_devs_dir = Path('/sys/class/net')
+ regex_filter = r'^/sys/devices/pci[\w/:\.]+/(?P<pci_addr>\w+:\w+:\w+\.\w+)/[\w/:\.]+/(?P<iface_name>eth\d+)$'
+ for dir in net_devs_dir.iterdir():
+ real_dir: str = dir.resolve().as_posix()
+ re_obj = re_fullmatch(regex_filter, real_dir)
+ if re_obj:
+ iface_name: str = re_obj.group('iface_name')
+ iface_addr: str = re_obj.group('pci_addr')
+ net_devs.update({iface_addr: iface_name})
+ # match to provided PCI address and return a name if found
+ if pci_addr in net_devs:
+ return net_devs[pci_addr]
+ # raise error if device was not found
+ raise FileNotFoundError(
+ f'PCI device {pci_addr} not found in ethernet interfaces')
+
+ @staticmethod
+ def rename_iface(name_old: str, name_new: str) -> None:
+ """Rename interface
+
+ Args:
+ name_old (str): old name
+ name_new (str): new name
+ """
+ rename_cmd: list[str] = [
+ 'ip', 'link', 'set', name_old, 'name', name_new
+ ]
+ run(rename_cmd)
diff --git a/python/vyos/xml_ref/definition.py b/python/vyos/xml_ref/definition.py
index 7fd7a7b77..33a49ca69 100644
--- a/python/vyos/xml_ref/definition.py
+++ b/python/vyos/xml_ref/definition.py
@@ -147,8 +147,8 @@ class Xml:
default = self._get_default_value(node)
if default is None:
return None
- if self._is_multi_node(node) and not isinstance(default, list):
- return [default]
+ if self._is_multi_node(node):
+ return default.split()
return default
def get_defaults(self, path: list, get_first_key=False, recursive=False) -> dict: