summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/setup.py49
-rw-r--r--python/vyos/accel_ppp.py2
-rw-r--r--python/vyos/accel_ppp_util.py6
-rw-r--r--python/vyos/airbag.py8
-rw-r--r--python/vyos/base.py21
-rw-r--r--python/vyos/component_version.py65
-rw-r--r--python/vyos/compose_config.py2
-rw-r--r--python/vyos/config.py24
-rw-r--r--python/vyos/config_mgmt.py43
-rw-r--r--python/vyos/configdep.py11
-rw-r--r--python/vyos/configdict.py66
-rw-r--r--python/vyos/configdiff.py2
-rw-r--r--python/vyos/configquery.py97
-rw-r--r--python/vyos/configsession.py167
-rw-r--r--python/vyos/configsource.py129
-rw-r--r--python/vyos/configtree.py116
-rw-r--r--python/vyos/configverify.py27
-rw-r--r--python/vyos/debug.py2
-rw-r--r--python/vyos/defaults.py39
-rw-r--r--python/vyos/ethtool.py2
-rwxr-xr-xpython/vyos/firewall.py99
-rw-r--r--python/vyos/frrender.py30
-rw-r--r--python/vyos/ifconfig/__init__.py2
-rw-r--r--python/vyos/ifconfig/afi.py2
-rw-r--r--python/vyos/ifconfig/bond.py24
-rw-r--r--python/vyos/ifconfig/bridge.py26
-rw-r--r--python/vyos/ifconfig/control.py8
-rw-r--r--python/vyos/ifconfig/dummy.py2
-rw-r--r--python/vyos/ifconfig/ethernet.py2
-rw-r--r--python/vyos/ifconfig/geneve.py4
-rw-r--r--python/vyos/ifconfig/input.py2
-rw-r--r--python/vyos/ifconfig/interface.py240
-rw-r--r--python/vyos/ifconfig/l2tpv3.py2
-rw-r--r--python/vyos/ifconfig/loopback.py2
-rw-r--r--python/vyos/ifconfig/macsec.py2
-rw-r--r--python/vyos/ifconfig/macvlan.py2
-rw-r--r--python/vyos/ifconfig/operational.py2
-rw-r--r--python/vyos/ifconfig/pppoe.py2
-rw-r--r--python/vyos/ifconfig/section.py2
-rw-r--r--python/vyos/ifconfig/sstpc.py2
-rw-r--r--python/vyos/ifconfig/tunnel.py2
-rw-r--r--python/vyos/ifconfig/veth.py2
-rw-r--r--python/vyos/ifconfig/vrrp.py2
-rw-r--r--python/vyos/ifconfig/vti.py2
-rw-r--r--python/vyos/ifconfig/vtun.py2
-rw-r--r--python/vyos/ifconfig/vxlan.py2
-rw-r--r--python/vyos/ifconfig/wireguard.py195
-rw-r--r--python/vyos/ifconfig/wireless.py2
-rw-r--r--python/vyos/ifconfig/wwan.py2
-rw-r--r--python/vyos/iflag.py2
-rw-r--r--python/vyos/include/__init__.py15
-rw-r--r--python/vyos/include/uapi/__init__.py15
-rw-r--r--python/vyos/include/uapi/linux/__init__.py15
-rw-r--r--python/vyos/include/uapi/linux/fib_rules.py20
-rw-r--r--python/vyos/include/uapi/linux/icmpv6.py18
-rw-r--r--python/vyos/include/uapi/linux/if_arp.py176
-rw-r--r--python/vyos/include/uapi/linux/lwtunnel.py38
-rw-r--r--python/vyos/include/uapi/linux/neighbour.py34
-rw-r--r--python/vyos/include/uapi/linux/rtnetlink.py63
-rw-r--r--python/vyos/initialsetup.py2
-rw-r--r--python/vyos/ioctl.py2
-rw-r--r--python/vyos/ipsec.py2
-rw-r--r--python/vyos/kea.py198
-rw-r--r--python/vyos/limericks.py2
-rw-r--r--python/vyos/load_config.py2
-rw-r--r--python/vyos/logger.py2
-rw-r--r--python/vyos/migrate.py2
-rw-r--r--python/vyos/nat.py2
-rw-r--r--python/vyos/opmode.py2
-rw-r--r--python/vyos/pki.py2
-rw-r--r--python/vyos/priority.py2
-rw-r--r--python/vyos/progressbar.py2
-rw-r--r--python/vyos/proto/__init__.py0
-rwxr-xr-xpython/vyos/proto/generate_dataclass.py178
-rw-r--r--python/vyos/proto/vycall_pb2.py29
-rw-r--r--python/vyos/proto/vyconf_client.py89
-rw-r--r--python/vyos/proto/vyconf_pb2.py93
-rw-r--r--python/vyos/proto/vyconf_proto.py379
-rw-r--r--python/vyos/qos/__init__.py2
-rw-r--r--python/vyos/qos/base.py2
-rw-r--r--python/vyos/qos/cake.py11
-rw-r--r--python/vyos/qos/droptail.py2
-rw-r--r--python/vyos/qos/fairqueue.py2
-rw-r--r--python/vyos/qos/fqcodel.py2
-rw-r--r--python/vyos/qos/limiter.py2
-rw-r--r--python/vyos/qos/netem.py2
-rw-r--r--python/vyos/qos/priority.py2
-rw-r--r--python/vyos/qos/randomdetect.py2
-rw-r--r--python/vyos/qos/ratelimiter.py2
-rw-r--r--python/vyos/qos/roundrobin.py2
-rw-r--r--python/vyos/qos/trafficshaper.py2
-rw-r--r--python/vyos/raid.py2
-rw-r--r--python/vyos/remote.py46
-rw-r--r--python/vyos/snmpv3_hashgen.py2
-rw-r--r--python/vyos/system/__init__.py2
-rw-r--r--python/vyos/system/compat.py2
-rw-r--r--python/vyos/system/disk.py2
-rw-r--r--python/vyos/system/grub.py2
-rw-r--r--python/vyos/system/grub_util.py7
-rw-r--r--python/vyos/system/image.py2
-rw-r--r--python/vyos/system/raid.py2
-rwxr-xr-xpython/vyos/template.py211
-rw-r--r--python/vyos/tpm.py2
-rw-r--r--python/vyos/utils/__init__.py2
-rw-r--r--python/vyos/utils/assertion.py2
-rw-r--r--python/vyos/utils/auth.py72
-rw-r--r--python/vyos/utils/backend.py94
-rw-r--r--python/vyos/utils/boot.py2
-rw-r--r--python/vyos/utils/commit.py80
-rw-r--r--python/vyos/utils/config.py2
-rw-r--r--python/vyos/utils/configfs.py2
-rw-r--r--python/vyos/utils/convert.py2
-rw-r--r--python/vyos/utils/cpu.py23
-rw-r--r--python/vyos/utils/dict.py39
-rw-r--r--python/vyos/utils/disk.py2
-rw-r--r--python/vyos/utils/error.py2
-rw-r--r--python/vyos/utils/file.py59
-rw-r--r--python/vyos/utils/io.py2
-rw-r--r--python/vyos/utils/kernel.py6
-rw-r--r--python/vyos/utils/list.py2
-rw-r--r--python/vyos/utils/locking.py2
-rw-r--r--python/vyos/utils/misc.py6
-rw-r--r--python/vyos/utils/network.py131
-rw-r--r--python/vyos/utils/permission.py26
-rw-r--r--python/vyos/utils/process.py50
-rw-r--r--python/vyos/utils/serial.py15
-rw-r--r--python/vyos/utils/session.py25
-rw-r--r--python/vyos/utils/strip_config.py2
-rw-r--r--python/vyos/utils/system.py2
-rw-r--r--python/vyos/utils/vti_updown_db.py2
-rw-r--r--python/vyos/version.py2
-rw-r--r--python/vyos/vpp/__init__.py20
-rw-r--r--python/vyos/vpp/acl/__init__.py3
-rw-r--r--python/vyos/vpp/acl/acl.py106
-rw-r--r--python/vyos/vpp/config_deps.py76
-rw-r--r--python/vyos/vpp/config_filter.py58
-rw-r--r--python/vyos/vpp/config_resource_checks/__init__.py0
-rw-r--r--python/vyos/vpp/config_resource_checks/cpu.py76
-rw-r--r--python/vyos/vpp/config_resource_checks/memory.py189
-rw-r--r--python/vyos/vpp/config_resource_checks/resource_defaults.py43
-rw-r--r--python/vyos/vpp/config_verify.py444
-rw-r--r--python/vyos/vpp/configdb.py206
-rw-r--r--python/vyos/vpp/control_host.py375
-rw-r--r--python/vyos/vpp/control_vpp.py488
-rw-r--r--python/vyos/vpp/interface/__init__.py42
-rw-r--r--python/vyos/vpp/interface/bond.py118
-rw-r--r--python/vyos/vpp/interface/bridge.py128
-rw-r--r--python/vyos/vpp/interface/ethernet.py53
-rw-r--r--python/vyos/vpp/interface/geneve.py90
-rw-r--r--python/vyos/vpp/interface/gre.py140
-rw-r--r--python/vyos/vpp/interface/interface.py49
-rw-r--r--python/vyos/vpp/interface/ipip.py94
-rw-r--r--python/vyos/vpp/interface/loopback.py72
-rw-r--r--python/vyos/vpp/interface/vxlan.py106
-rw-r--r--python/vyos/vpp/interface/wireguard.py55
-rw-r--r--python/vyos/vpp/interface/xconnect.py95
-rw-r--r--python/vyos/vpp/nat/__init__.py4
-rw-r--r--python/vyos/vpp/nat/det44.py106
-rw-r--r--python/vyos/vpp/nat/nat44.py229
-rw-r--r--python/vyos/vpp/utils.py417
-rw-r--r--python/vyos/vyconf_session.py233
-rw-r--r--python/vyos/wanloadbalance.py153
-rw-r--r--python/vyos/xml_ref/__init__.py30
-rw-r--r--python/vyos/xml_ref/definition.py2
-rwxr-xr-xpython/vyos/xml_ref/generate_cache.py2
-rwxr-xr-xpython/vyos/xml_ref/generate_op_cache.py301
-rw-r--r--python/vyos/xml_ref/op_definition.py249
-rwxr-xr-xpython/vyos/xml_ref/update_cache.py2
168 files changed, 8139 insertions, 620 deletions
diff --git a/python/setup.py b/python/setup.py
index 2d614e724..571b956ee 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -1,5 +1,14 @@
import os
+import sys
+import subprocess
from setuptools import setup
+from setuptools.command.build_py import build_py
+
+sys.path.append('./vyos')
+from defaults import directories
+
+def desc_out(f):
+ return os.path.splitext(f)[0] + '.desc'
def packages(directory):
return [
@@ -8,6 +17,43 @@ def packages(directory):
if os.path.isfile(os.path.join(_[0], '__init__.py'))
]
+
+class GenerateProto(build_py):
+ ver = os.environ.get('OCAML_VERSION')
+ if ver:
+ proto_path = f'/opt/opam/{ver}/share/vyconf'
+ else:
+ proto_path = directories['proto_path']
+
+ def run(self):
+ # find all .proto files in vyconf proto_path
+ proto_files = []
+ for _, _, files in os.walk(self.proto_path):
+ for file in files:
+ if file.endswith('.proto'):
+ proto_files.append(file)
+
+ # compile each .proto file to Python
+ for proto_file in proto_files:
+ subprocess.check_call(
+ [
+ 'protoc',
+ '--python_out=vyos/proto',
+ f'--proto_path={self.proto_path}/',
+ f'--descriptor_set_out=vyos/proto/{desc_out(proto_file)}',
+ proto_file,
+ ]
+ )
+ subprocess.check_call(
+ [
+ 'vyos/proto/generate_dataclass.py',
+ 'vyos/proto/vyconf.desc',
+ '--out-dir=vyos/proto',
+ ]
+ )
+
+ build_py.run(self)
+
setup(
name = "vyos",
version = "1.3.0",
@@ -29,4 +75,7 @@ setup(
"config-mgmt = vyos.config_mgmt:run",
],
},
+ cmdclass={
+ 'build_py': GenerateProto,
+ },
)
diff --git a/python/vyos/accel_ppp.py b/python/vyos/accel_ppp.py
index bae695fc3..b1160dc76 100644
--- a/python/vyos/accel_ppp.py
+++ b/python/vyos/accel_ppp.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
diff --git a/python/vyos/accel_ppp_util.py b/python/vyos/accel_ppp_util.py
index ae75e6654..85e8a964c 100644
--- a/python/vyos/accel_ppp_util.py
+++ b/python/vyos/accel_ppp_util.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -221,10 +221,12 @@ def verify_accel_ppp_ip_pool(vpn_config):
for interface, interface_config in vpn_config['interface'].items():
if dict_search('client_subnet', interface_config):
break
+ if dict_search('external_dhcp.dhcp_relay', interface_config):
+ break
else:
raise ConfigError(
'Local auth and noauth mode requires local client-ip-pool \
- or client-ipv6-pool or client-subnet to be configured!')
+ or client-ipv6-pool or client-subnet or dhcp-relay to be configured!')
else:
raise ConfigError(
"Local auth mode requires local client-ip-pool \
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
index 3c7a144b7..1dcccdd47 100644
--- a/python/vyos/airbag.py
+++ b/python/vyos/airbag.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -15,10 +15,12 @@
import sys
from datetime import datetime
+from collections import deque
from vyos import debug
from vyos.logger import syslog
from vyos.version import get_full_version_data
+from vyos.defaults import airbag_noteworthy_size
def enable(log=True):
@@ -27,7 +29,7 @@ def enable(log=True):
_intercepting_exceptions()
-_noteworthy = []
+_noteworthy = deque(maxlen=airbag_noteworthy_size)
def noteworthy(msg):
@@ -72,7 +74,7 @@ def bug_report(dtype, value, trace):
note = ''
if _noteworthy:
note = 'noteworthy:\n'
- note += '\n'.join(_noteworthy)
+ note += '\n'.join(list(_noteworthy))
information.update({
'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
diff --git a/python/vyos/base.py b/python/vyos/base.py
index ca96d96ce..67f92564e 100644
--- a/python/vyos/base.py
+++ b/python/vyos/base.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -15,8 +15,7 @@
from textwrap import fill
-
-class BaseWarning:
+class UserMessage:
def __init__(self, header, message, **kwargs):
self.message = message
self.kwargs = kwargs
@@ -33,7 +32,6 @@ class BaseWarning:
messages = self.message.split('\n')
isfirstmessage = True
initial_indent = self.textinitindent
- print('')
for mes in messages:
mes = fill(mes, initial_indent=initial_indent,
subsequent_indent=self.standardindent, **self.kwargs)
@@ -44,17 +42,24 @@ class BaseWarning:
print('', flush=True)
+class Message():
+ def __init__(self, message, **kwargs):
+ self.Message = UserMessage('', message, **kwargs)
+ self.Message.print()
+
class Warning():
def __init__(self, message, **kwargs):
- self.BaseWarn = BaseWarning('WARNING: ', message, **kwargs)
- self.BaseWarn.print()
+ print('')
+ self.UserMessage = UserMessage('WARNING: ', message, **kwargs)
+ self.UserMessage.print()
class DeprecationWarning():
def __init__(self, message, **kwargs):
# Reformat the message and trim it to 72 characters in length
- self.BaseWarn = BaseWarning('DEPRECATION WARNING: ', message, **kwargs)
- self.BaseWarn.print()
+ print('')
+ self.UserMessage = UserMessage('DEPRECATION WARNING: ', message, **kwargs)
+ self.UserMessage.print()
class ConfigError(Exception):
diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py
index 94215531d..136bd36e8 100644
--- a/python/vyos/component_version.py
+++ b/python/vyos/component_version.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -49,7 +49,9 @@ DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot')
REGEX_WARN_VYOS = r'(// Warning: Do not remove the following line.)'
REGEX_WARN_VYATTA = r'(/\* Warning: Do not remove the following line. \*/)'
REGEX_COMPONENT_VERSION_VYOS = r'// vyos-config-version:\s+"([\w@:-]+)"\s*'
-REGEX_COMPONENT_VERSION_VYATTA = r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/'
+REGEX_COMPONENT_VERSION_VYATTA = (
+ r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/'
+)
REGEX_RELEASE_VERSION_VYOS = r'// Release version:\s+(\S*)\s*'
REGEX_RELEASE_VERSION_VYATTA = r'/\* Release version:\s+(\S*)\s*\*/'
@@ -62,16 +64,31 @@ CONFIG_FILE_VERSION = """\
warn_filter_vyos = re.compile(REGEX_WARN_VYOS)
warn_filter_vyatta = re.compile(REGEX_WARN_VYATTA)
-regex_filter = { 'vyos': dict(zip(['component', 'release'],
- [re.compile(REGEX_COMPONENT_VERSION_VYOS),
- re.compile(REGEX_RELEASE_VERSION_VYOS)])),
- 'vyatta': dict(zip(['component', 'release'],
- [re.compile(REGEX_COMPONENT_VERSION_VYATTA),
- re.compile(REGEX_RELEASE_VERSION_VYATTA)])) }
+regex_filter = {
+ 'vyos': dict(
+ zip(
+ ['component', 'release'],
+ [
+ re.compile(REGEX_COMPONENT_VERSION_VYOS),
+ re.compile(REGEX_RELEASE_VERSION_VYOS),
+ ],
+ )
+ ),
+ 'vyatta': dict(
+ zip(
+ ['component', 'release'],
+ [
+ re.compile(REGEX_COMPONENT_VERSION_VYATTA),
+ re.compile(REGEX_RELEASE_VERSION_VYATTA),
+ ],
+ )
+ ),
+}
+
@dataclass
class VersionInfo:
- component: Optional[dict[str,int]] = None
+ component: Optional[dict[str, int]] = None
release: str = get_version()
vintage: str = 'vyos'
config_body: Optional[str] = None
@@ -84,8 +101,9 @@ class VersionInfo:
return bool(self.config_body is None)
def update_footer(self):
- f = CONFIG_FILE_VERSION.format(component_to_string(self.component),
- self.release)
+ f = CONFIG_FILE_VERSION.format(
+ component_to_string(self.component), self.release
+ )
self.footer_lines = f.splitlines()
def update_syntax(self):
@@ -121,13 +139,16 @@ class VersionInfo:
except Exception as e:
raise ValueError(e) from e
+
def component_to_string(component: dict) -> str:
- l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])]
+ l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])] # noqa: E741
return ':'.join(l)
+
def component_from_string(string: str) -> dict:
return {k: int(v) for k, v in re.findall(r'([\w,-]+)@(\d+)', string)}
+
def version_info_from_file(config_file) -> VersionInfo:
"""Return config file component and release version info."""
version_info = VersionInfo()
@@ -166,27 +187,27 @@ def version_info_from_file(config_file) -> VersionInfo:
return version_info
+
def version_info_from_system() -> VersionInfo:
"""Return system component and release version info."""
d = component_version()
sort_d = dict(sorted(d.items(), key=lambda x: x[0]))
- version_info = VersionInfo(
- component = sort_d,
- release = get_version(),
- vintage = 'vyos'
- )
+ version_info = VersionInfo(component=sort_d, release=get_version(), vintage='vyos')
return version_info
+
def version_info_copy(v: VersionInfo) -> VersionInfo:
"""Make a copy of dataclass."""
return replace(v)
+
def version_info_prune_component(x: VersionInfo, y: VersionInfo) -> VersionInfo:
"""In place pruning of component keys of x not in y."""
if x.component is None or y.component is None:
return
- x.component = { k: v for k,v in x.component.items() if k in y.component }
+ x.component = {k: v for k, v in x.component.items() if k in y.component}
+
def add_system_version(config_str: str = None, out_file: str = None):
"""Wrap config string with system version and write to out_file.
@@ -202,3 +223,11 @@ def add_system_version(config_str: str = None, out_file: str = None):
version_info.write(out_file)
else:
sys.stdout.write(version_info.write_string())
+
+
+def append_system_version(file: str):
+ """Append system version data to existing file"""
+ version_info = version_info_from_system()
+ version_info.update_footer()
+ with open(file, 'a') as f:
+ f.write(version_info.write_string())
diff --git a/python/vyos/compose_config.py b/python/vyos/compose_config.py
index 79a8718c5..1e7837858 100644
--- a/python/vyos/compose_config.py
+++ b/python/vyos/compose_config.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 1fab46761..6f7c76ca7 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -1,4 +1,4 @@
-# Copyright 2017-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -73,8 +73,11 @@ from vyos.xml_ref import ext_dict_merge
from vyos.xml_ref import relative_defaults
from vyos.utils.dict import get_sub_dict
from vyos.utils.dict import mangle_dict_keys
+from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.backend import vyconf_backend
from vyos.configsource import ConfigSource
from vyos.configsource import ConfigSourceSession
+from vyos.configsource import ConfigSourceVyconfSession
class ConfigDict(dict):
_from_defaults = {}
@@ -131,8 +134,13 @@ class Config(object):
subtrees.
"""
def __init__(self, session_env=None, config_source=None):
+ self.vyconf_session = None
if config_source is None:
- self._config_source = ConfigSourceSession(session_env)
+ if vyconf_backend() and boot_configuration_complete():
+ self._config_source = ConfigSourceVyconfSession(session_env)
+ self.vyconf_session = self._config_source._vyconf_session
+ else:
+ self._config_source = ConfigSourceSession(session_env)
else:
if not isinstance(config_source, ConfigSource):
raise TypeError("config_source not of type ConfigSource")
@@ -149,6 +157,18 @@ class Config(object):
return self._running_config
return self._session_config
+ def get_bool_attr(self, attr) -> bool:
+ if not hasattr(self, attr):
+ return False
+ else:
+ tmp = getattr(self, attr)
+ if not isinstance(tmp, bool):
+ return False
+ return tmp
+
+ def set_bool_attr(self, attr, val):
+ setattr(self, attr, val)
+
def _make_path(self, path):
# Backwards-compatibility stuff: original implementation used string paths
# libvyosconfig paths are lists, but since node names cannot contain whitespace,
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index 1c2b70fdf..b549b3005 100644
--- a/python/vyos/config_mgmt.py
+++ b/python/vyos/config_mgmt.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -25,10 +25,12 @@ from filecmp import cmp
from datetime import datetime
from textwrap import dedent
from pathlib import Path
-from tabulate import tabulate
from shutil import copy, chown
+from subprocess import Popen
+from subprocess import DEVNULL
from urllib.parse import urlsplit
from urllib.parse import urlunsplit
+from tabulate import tabulate
from vyos.config import Config
from vyos.configtree import ConfigTree
@@ -44,6 +46,7 @@ from vyos.utils.io import ask_yes_no
from vyos.utils.boot import boot_configuration_complete
from vyos.utils.process import is_systemd_service_active
from vyos.utils.process import rc_cmd
+from vyos.defaults import DEFAULT_COMMIT_CONFIRM_MINUTES
SAVE_CONFIG = '/usr/libexec/vyos/vyos-save-config.py'
config_json = '/run/vyatta/config/config.json'
@@ -56,7 +59,6 @@ commit_hooks = {
'commit_archive': '02vyos-commit-archive',
}
-DEFAULT_TIME_MINUTES = 10
timer_name = 'commit-confirm'
config_file = os.path.join(directories['config'], 'config.boot')
@@ -144,14 +146,16 @@ class ConfigMgmt:
['system', 'config-management'],
key_mangling=('-', '_'),
get_first_key=True,
- with_defaults=True,
+ with_recursive_defaults=True,
)
self.max_revisions = int(d.get('commit_revisions', 0))
self.num_revisions = 0
self.locations = d.get('commit_archive', {}).get('location', [])
self.source_address = d.get('commit_archive', {}).get('source_address', '')
- self.reboot_unconfirmed = bool(d.get('commit_confirm') == 'reboot')
+ self.reboot_unconfirmed = bool(
+ d.get('commit_confirm', {}).get('action') == 'reboot'
+ )
self.config_dict = d
if config.exists(['system', 'host-name']):
@@ -181,7 +185,7 @@ class ConfigMgmt:
# Console script functions
#
def commit_confirm(
- self, minutes: int = DEFAULT_TIME_MINUTES, no_prompt: bool = False
+ self, minutes: int = DEFAULT_COMMIT_CONFIRM_MINUTES, no_prompt: bool = False
) -> Tuple[str, int]:
"""Commit with reload/reboot to saved config in 'minutes' minutes if
'confirm' call is not issued.
@@ -229,7 +233,14 @@ Proceed ?"""
else:
cmd = f'sudo -b /usr/libexec/vyos/commit-confirm-notify.py {minutes}'
- os.system(cmd)
+ Popen(
+ cmd.split(),
+ stdout=DEVNULL,
+ stderr=DEVNULL,
+ stdin=DEVNULL,
+ close_fds=True,
+ preexec_fn=os.setsid,
+ )
if self.reboot_unconfirmed:
msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reboot'
@@ -287,7 +298,7 @@ Proceed ?"""
# commits under commit-confirm are not added to revision list unless
# confirmed, hence a soft revert is to revision 0
- revert_ct = self._get_config_tree_revision(0)
+ revert_ct = self.get_config_tree_revision(0)
message = '[commit-confirm] Reverting to previous config now'
os.system('wall -n ' + message)
@@ -296,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
@@ -351,7 +366,7 @@ Proceed ?"""
)
return msg, 1
- rollback_ct = self._get_config_tree_revision(rev)
+ rollback_ct = self.get_config_tree_revision(rev)
try:
load(rollback_ct, switch='explicit')
print('Rollback diff has been applied.')
@@ -382,7 +397,7 @@ Proceed ?"""
if rev1 is not None:
if not self._check_revision_number(rev1):
return f'Invalid revision number {rev1}', 1
- ct1 = self._get_config_tree_revision(rev1)
+ ct1 = self.get_config_tree_revision(rev1)
ct2 = self.working_config
msg = f'No changes between working and revision {rev1} configurations.\n'
if rev2 is not None:
@@ -390,7 +405,7 @@ Proceed ?"""
return f'Invalid revision number {rev2}', 1
# compare older to newer
ct2 = ct1
- ct1 = self._get_config_tree_revision(rev2)
+ ct1 = self.get_config_tree_revision(rev2)
msg = f'No changes between revisions {rev2} and {rev1} configurations.\n'
out = ''
@@ -575,7 +590,7 @@ Proceed ?"""
r = f.read().decode()
return r
- def _get_config_tree_revision(self, rev: int):
+ def get_config_tree_revision(self, rev: int):
c = self._get_file_revision(rev)
return ConfigTree(c)
@@ -805,7 +820,7 @@ def run():
'-t',
dest='minutes',
type=int,
- default=DEFAULT_TIME_MINUTES,
+ default=DEFAULT_COMMIT_CONFIRM_MINUTES,
help="Minutes until reboot, unless 'confirm'",
)
commit_confirm.add_argument(
diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py
index cf7c9d543..04de66493 100644
--- a/python/vyos/configdep.py
+++ b/python/vyos/configdep.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -102,11 +102,16 @@ def run_config_mode_script(target: str, config: 'Config'):
mod = load_as_module(name, path)
config.set_level([])
+ dry_run = config.get_bool_attr('dry_run')
try:
c = mod.get_config(config)
mod.verify(c)
- mod.generate(c)
- mod.apply(c)
+ if not dry_run:
+ mod.generate(c)
+ mod.apply(c)
+ else:
+ if hasattr(mod, 'call_dependents'):
+ mod.call_dependents()
except (VyOSError, ConfigError) as e:
raise ConfigError(str(e)) from e
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 5a353b110..d91d88d88 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -491,10 +491,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
# Check if any DHCP options changed which require a client restat
dhcp = is_node_changed(config, base + [ifname, 'dhcp-options'])
if dhcp: dict.update({'dhcp_options_changed' : {}})
-
- # Changine interface VRF assignemnts require a DHCP restart, too
- dhcp = is_node_changed(config, base + [ifname, 'vrf'])
- if dhcp: dict.update({'dhcp_options_changed' : {}})
+ dhcpv6 = is_node_changed(config, base + [ifname, 'dhcpv6-options'])
+ if dhcpv6: dict.update({'dhcpv6_options_changed' : {}})
# Some interfaces come with a source_interface which must also not be part
# of any other bond or bridge interface as it is exclusivly assigned as the
@@ -519,6 +517,14 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
else:
dict['ipv6']['address'].update({'eui64_old': eui64})
+ interface_identifier = leaf_node_changed(config, base + [ifname, 'ipv6', 'address', 'interface-identifier'])
+ if interface_identifier:
+ tmp = dict_search('ipv6.address', dict)
+ if not tmp:
+ dict.update({'ipv6': {'address': {'interface_identifier_old': interface_identifier}}})
+ else:
+ dict['ipv6']['address'].update({'interface_identifier_old': interface_identifier})
+
for vif, vif_config in dict.get('vif', {}).items():
# Add subinterface name to dictionary
dict['vif'][vif].update({'ifname' : f'{ifname}.{vif}'})
@@ -543,6 +549,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
# Check if any DHCP options changed which require a client restat
dhcp = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcp-options'])
if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : {}})
+ dhcpv6 = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcpv6-options'])
+ if dhcpv6: dict['vif'][vif].update({'dhcpv6_options_changed' : {}})
for vif_s, vif_s_config in dict.get('vif_s', {}).items():
# Add subinterface name to dictionary
@@ -569,6 +577,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
# Check if any DHCP options changed which require a client restat
dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcp-options'])
if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : {}})
+ dhcpv6 = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcpv6-options'])
+ if dhcpv6: dict['vif_s'][vif_s].update({'dhcpv6_options_changed' : {}})
for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
# Add subinterface name to dictionary
@@ -597,6 +607,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
# Check if any DHCP options changed which require a client restat
dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options'])
if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : {}})
+ dhcpv6 = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcpv6-options'])
+ if dhcpv6: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcpv6_options_changed' : {}})
# Check vif, vif-s/vif-c VLAN interfaces for removal
dict = get_removed_vlans(config, base + [ifname], dict)
@@ -622,6 +634,23 @@ def get_vlan_ids(interface):
return vlan_ids
+def get_vlans_ids_and_range(interface):
+ vlan_ids = set()
+
+ vlan_filter_status = json.loads(cmd(f'bridge -j -d vlan show dev {interface}'))
+
+ if vlan_filter_status is not None:
+ for interface_status in vlan_filter_status:
+ for vlan_entry in interface_status.get("vlans", []):
+ start = vlan_entry["vlan"]
+ end = vlan_entry.get("vlanEnd")
+ if end:
+ vlan_ids.add(f"{start}-{end}")
+ else:
+ vlan_ids.add(str(start))
+
+ return vlan_ids
+
def get_accel_dict(config, base, chap_secrets, with_pki=False):
"""
Common utility function to retrieve and mangle the Accel-PPP configuration
@@ -632,6 +661,7 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):
Return a dictionary with the necessary interface config keys.
"""
from vyos.utils.cpu import get_core_count
+ from vyos.utils.cpu import get_half_cpus
from vyos.template import is_ipv4
dict = config.get_config_dict(base, key_mangling=('-', '_'),
@@ -641,7 +671,16 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):
with_pki=with_pki)
# set CPUs cores to process requests
- dict.update({'thread_count' : get_core_count()})
+ match dict.get('thread_count'):
+ case 'all':
+ dict['thread_count'] = get_core_count()
+ case 'half':
+ dict['thread_count'] = get_half_cpus()
+ case str(x) if x.isdigit():
+ dict['thread_count'] = int(x)
+ case _:
+ dict['thread_count'] = get_core_count()
+
# we need to store the path to the secrets file
dict.update({'chap_secrets_file' : chap_secrets})
@@ -664,3 +703,18 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):
dict['authentication']['radius']['server'][server]['acct_port'] = '0'
return dict
+
+def get_flowtable_interfaces(config):
+ """
+ Return all interfaces used in flowtables
+ """
+ ft_base = ['firewall', 'flowtable']
+
+ if not config.exists(ft_base):
+ return []
+
+ ifaces = []
+ for ft_name in config.list_nodes(ft_base):
+ ifaces += config.return_values(ft_base + [ft_name, 'interface'])
+
+ return ifaces
diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py
index b6d4a5558..5e21a16e5 100644
--- a/python/vyos/configdiff.py
+++ b/python/vyos/configdiff.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/configquery.py b/python/vyos/configquery.py
index 5d6ca9be9..b661b1a9e 100644
--- a/python/vyos/configquery.py
+++ b/python/vyos/configquery.py
@@ -1,4 +1,4 @@
-# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -13,10 +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/>.
-'''
+"""
A small library that allows querying existence or value(s) of config
settings from op mode, and execution of arbitrary op mode commands.
-'''
+"""
import os
import json
@@ -37,12 +37,16 @@ from vyos.utils.error import cli_shell_api_err
from vyos.xml_ref import multi_to_list
from vyos.xml_ref import is_tag
from vyos.base import Warning
+from vyos.utils.backend import vyconf_backend
+from vyos.configsource import ConfigSourceVyconfSession
config_file = os.path.join(directories['config'], 'config.boot')
+
class ConfigQueryError(Exception):
pass
+
class GenericConfigQuery:
def __init__(self):
pass
@@ -56,6 +60,7 @@ class GenericConfigQuery:
def values(self, path: list):
raise NotImplementedError
+
class GenericOpRun:
def __init__(self):
pass
@@ -63,6 +68,7 @@ class GenericOpRun:
def run(self, path: list, **kwargs):
raise NotImplementedError
+
class CliShellApiConfigQuery(GenericConfigQuery):
def __init__(self):
super().__init__()
@@ -88,22 +94,27 @@ class CliShellApiConfigQuery(GenericConfigQuery):
raise ConfigQueryError('No values for given path')
return out
+
class ConfigTreeQuery(GenericConfigQuery):
def __init__(self):
super().__init__()
if boot_configuration_complete():
- config_source = ConfigSourceSession()
+ if vyconf_backend():
+ config_source = ConfigSourceVyconfSession()
+ else:
+ config_source = ConfigSourceSession()
self.config = Config(config_source=config_source)
else:
try:
with open(config_file) as f:
config_string = f.read()
- except OSError as err:
+ except OSError:
config_string = ''
- config_source = ConfigSourceString(running_config_text=config_string,
- session_config_text=config_string)
+ config_source = ConfigSourceString(
+ running_config_text=config_string, session_config_text=config_string
+ )
self.config = Config(config_source=config_source)
def exists(self, path: list):
@@ -118,13 +129,28 @@ class ConfigTreeQuery(GenericConfigQuery):
def list_nodes(self, path: list):
return self.config.list_nodes(path)
- def get_config_dict(self, path=[], effective=False, key_mangling=None,
- get_first_key=False, no_multi_convert=False,
- no_tag_node_value_mangle=False):
- return self.config.get_config_dict(path, effective=effective,
- key_mangling=key_mangling, get_first_key=get_first_key,
- no_multi_convert=no_multi_convert,
- no_tag_node_value_mangle=no_tag_node_value_mangle)
+ def get_config_dict(
+ self,
+ path=[],
+ effective=False,
+ key_mangling=None,
+ get_first_key=False,
+ no_multi_convert=False,
+ no_tag_node_value_mangle=False,
+ with_defaults=False,
+ with_recursive_defaults=False,
+ ):
+ return self.config.get_config_dict(
+ path,
+ effective=effective,
+ key_mangling=key_mangling,
+ get_first_key=get_first_key,
+ no_multi_convert=no_multi_convert,
+ no_tag_node_value_mangle=no_tag_node_value_mangle,
+ with_defaults=with_defaults,
+ with_recursive_defaults=with_recursive_defaults,
+ )
+
class VbashOpRun(GenericOpRun):
def __init__(self):
@@ -132,35 +158,45 @@ class VbashOpRun(GenericOpRun):
def run(self, path: list, **kwargs):
cmd = ' '.join(path)
- (out, err) = popen(f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}', stderr=STDOUT, **kwargs)
+ (out, err) = popen(
+ f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}', stderr=STDOUT, **kwargs
+ )
if err:
raise ConfigQueryError(out)
return out
-def query_context(config_query_class=CliShellApiConfigQuery,
- op_run_class=VbashOpRun):
+
+def query_context(config_query_class=CliShellApiConfigQuery, op_run_class=VbashOpRun):
query = config_query_class()
run = op_run_class()
return query, run
+
def verify_mangling(key_mangling):
- if not (isinstance(key_mangling, tuple) and
- len(key_mangling) == 2 and
- isinstance(key_mangling[0], str) and
- isinstance(key_mangling[1], str)):
- raise ValueError("key_mangling must be a tuple of two strings")
+ if not (
+ isinstance(key_mangling, tuple)
+ and len(key_mangling) == 2
+ and isinstance(key_mangling[0], str)
+ and isinstance(key_mangling[1], str)
+ ):
+ raise ValueError('key_mangling must be a tuple of two strings')
+
def op_mode_run(cmd):
- """ low-level to avoid overhead """
+ """low-level to avoid overhead"""
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
out = p.stdout.read()
p.wait()
return p.returncode, out.decode()
-def op_mode_config_dict(path=None, key_mangling=None,
- no_tag_node_value_mangle=False,
- no_multi_convert=False, get_first_key=False):
+def op_mode_config_dict(
+ path=None,
+ key_mangling=None,
+ no_tag_node_value_mangle=False,
+ no_multi_convert=False,
+ get_first_key=False,
+):
if path is None:
path = []
command = ['/bin/cli-shell-api', '--show-active-only', 'showConfig']
@@ -185,8 +221,11 @@ def op_mode_config_dict(path=None, key_mangling=None,
if key_mangling is not None:
verify_mangling(key_mangling)
- config_dict = mangle_dict_keys(config_dict,
- key_mangling[0], key_mangling[1],
- no_tag_node_value_mangle=no_tag_node_value_mangle)
+ config_dict = mangle_dict_keys(
+ config_dict,
+ key_mangling[0],
+ key_mangling[1],
+ no_tag_node_value_mangle=no_tag_node_value_mangle,
+ )
return get_sub_dict(config_dict, path, get_first_key=get_first_key)
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index dd3ad1e3d..cd0088b60 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2019-2024 VyOS maintainers and contributors
+# Copyright 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;
@@ -17,20 +17,36 @@ 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
from vyos.utils.dict import dict_to_paths
+from vyos.utils.boot import boot_configuration_complete
+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'
-SET = '/usr/libexec/vyos/vyconf/vy_set'
-DELETE = '/usr/libexec/vyos/vyconf/vy_delete'
+SET = '/opt/vyatta/sbin/my_set'
+DELETE = '/opt/vyatta/sbin/my_delete'
COMMENT = '/opt/vyatta/sbin/my_comment'
COMMIT = '/opt/vyatta/sbin/my_commit'
+COMMIT_CONFIRM = ['/usr/bin/config-mgmt', 'commit_confirm', '-y']
+CONFIRM = ['/usr/bin/config-mgmt', 'confirm']
DISCARD = '/opt/vyatta/sbin/my_discard'
SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
MIGRATE_LOAD_CONFIG = ['/usr/libexec/vyos/vyos-load-config.py']
+MERGE_CONFIG = ['/usr/libexec/vyos/vyos-merge-config.py']
SAVE_CONFIG = ['/usr/libexec/vyos/vyos-save-config.py']
INSTALL_IMAGE = [
'/usr/libexec/vyos/op_mode/image_installer.py',
@@ -63,6 +79,7 @@ GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show']
RESET = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reset']
REBOOT = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reboot']
+RENEW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'renew']
POWEROFF = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'poweroff']
OP_CMD_ADD = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'add']
OP_CMD_DELETE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'delete']
@@ -116,6 +133,10 @@ def inject_vyos_env(env):
env['vyos_sbin_dir'] = '/usr/sbin'
env['vyos_validators_dir'] = '/usr/libexec/vyos/validators'
+ # with the retirement of the Cstore backend, this will remain as the
+ # sole indication of legacy CLI config mode, as checked by VyconfSession
+ env['_OFR_CONFIGURE'] = 'ok'
+
# if running the vyos-configd daemon, inject the vyshim env var
if is_systemd_service_running('vyos-configd.service'):
env['vyshim'] = '/usr/sbin/vyshim'
@@ -132,7 +153,7 @@ class ConfigSession(object):
The write API of VyOS.
"""
- def __init__(self, session_id, app=APP):
+ def __init__(self, session_id, app=APP, shared=False):
"""
Creates a new config session.
@@ -160,32 +181,52 @@ class ConfigSession(object):
for k, v in env_list:
session_env[k] = v
+ session_env['CONFIGSESSION_PID'] = str(session_id)
+
self.__session_env = session_env
self.__session_env['COMMIT_VIA'] = app
self.__run_command([CLI_SHELL_API, 'setupSession'])
+ if vyconf_backend() and boot_configuration_complete():
+ self._vyconf_session = VyconfSession(
+ pid=session_id, on_error=ConfigSessionError
+ )
+ else:
+ self._vyconf_session = None
+
+ self.shared = shared
+
def __del__(self):
- try:
- output = (
- subprocess.check_output(
- [CLI_SHELL_API, 'teardownSession'], env=self.__session_env
+ if self.shared:
+ return
+ if self._vyconf_session is None:
+ try:
+ output = (
+ subprocess.check_output(
+ [CLI_SHELL_API, 'teardownSession'], env=self.__session_env
+ )
+ .decode()
+ .strip()
)
- .decode()
- .strip()
- )
- if output:
+ if output:
+ print(
+ 'cli-shell-api teardownSession output for sesion {0}: {1}'.format(
+ self.__session_id, output
+ ),
+ file=sys.stderr,
+ )
+ except Exception as e:
print(
- 'cli-shell-api teardownSession output for sesion {0}: {1}'.format(
- self.__session_id, output
- ),
+ 'Could not tear down session {0}: {1}'.format(self.__session_id, e),
file=sys.stderr,
)
- except Exception as e:
- print(
- 'Could not tear down session {0}: {1}'.format(self.__session_id, e),
- file=sys.stderr,
- )
+ else:
+ if self._vyconf_session.session_changed():
+ Warn('Exiting with uncommitted changes')
+ self._vyconf_session.discard()
+ self._vyconf_session.exit_config_mode()
+ self._vyconf_session.teardown()
def __run_command(self, cmd_list):
p = subprocess.Popen(
@@ -204,12 +245,18 @@ 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 = []
else:
value = [value]
- self.__run_command([SET] + path + value)
+ if self._vyconf_session is None:
+ self.__run_command([SET] + path + value)
+ else:
+ self._vyconf_session.set(path + value)
def set_section(self, path: list, d: dict):
try:
@@ -223,7 +270,10 @@ class ConfigSession(object):
value = []
else:
value = [value]
- self.__run_command([DELETE] + path + value)
+ if self._vyconf_session is None:
+ self.__run_command([DELETE] + path + value)
+ else:
+ self._vyconf_session.delete(path + value)
def load_section(self, path: list, d: dict):
try:
@@ -261,20 +311,46 @@ class ConfigSession(object):
self.__run_command([COMMENT] + path + value)
def commit(self):
- out = self.__run_command([COMMIT])
+ if self._vyconf_session is None:
+ out = self.__run_command([COMMIT])
+ else:
+ out, _ = self._vyconf_session.commit()
+
+ return out
+
+ def commit_confirm(self, minutes: int = DEFAULT_COMMIT_CONFIRM_MINUTES):
+ out = self.__run_command(COMMIT_CONFIRM + [f'-t {minutes}'])
+
+ return out
+
+ def confirm(self):
+ out = self.__run_command(CONFIRM)
+
return out
def discard(self):
- self.__run_command([DISCARD])
+ if self._vyconf_session is None:
+ self.__run_command([DISCARD])
+ else:
+ out, _ = self._vyconf_session.discard()
def show_config(self, path, format='raw'):
- config_data = self.__run_command(SHOW_CONFIG + path)
+ if self._vyconf_session is None:
+ config_data = self.__run_command(SHOW_CONFIG + path)
+ else:
+ config_data, _ = self._vyconf_session.show_config(path)
if format == 'raw':
return config_data
- def load_config(self, file_path):
- out = self.__run_command(LOAD_CONFIG + [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, cached=cached
+ )
+
return out
def load_explicit(self, file_path):
@@ -286,12 +362,41 @@ 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):
- out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path])
+ if self._vyconf_session is None:
+ out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path])
+ else:
+ out, _ = self._vyconf_session.load_config(file_name=file_path, migrate=True)
+
+ return out
+
+ def merge_config(self, file_path, destructive=False):
+ if self._vyconf_session is None:
+ destr = ['--destructive'] if destructive else []
+ out = self.__run_command(MERGE_CONFIG + [file_path] + destr)
+ else:
+ out, _ = self._vyconf_session.merge_config(
+ file_name=file_path, destructive=destructive
+ )
+
return out
def save_config(self, file_path):
- out = self.__run_command(SAVE_CONFIG + [file_path])
+ if self._vyconf_session is None:
+ out = self.__run_command(SAVE_CONFIG + [file_path])
+ else:
+ out, _ = self._vyconf_session.save_config(
+ file=file_path, append_version=True
+ )
+
return out
def install_image(self, url):
@@ -330,6 +435,10 @@ class ConfigSession(object):
out = self.__run_command(RESET + path)
return out
+ def renew(self, path):
+ out = self.__run_command(RENEW + path)
+ return out
+
def poweroff(self, path):
out = self.__run_command(POWEROFF + path)
return out
diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py
index 59e5ac8a1..24a17c711 100644
--- a/python/vyos/configsource.py
+++ b/python/vyos/configsource.py
@@ -1,5 +1,5 @@
-# Copyright 2020-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -17,9 +17,16 @@
import os
import re
import subprocess
+from typing import Union
from vyos.configtree import ConfigTree
from vyos.utils.boot import boot_configuration_complete
+from vyos.vyconf_session import VyconfSession
+from vyos.vyconf_session import VyconfSessionError
+from vyos.defaults import directories
+from vyos.xml_ref import is_tag
+from vyos.xml_ref import is_leaf
+from vyos.xml_ref import is_multi
class VyOSError(Exception):
"""
@@ -310,6 +317,116 @@ class ConfigSourceSession(ConfigSource):
except VyOSError:
return False
+class ConfigSourceVyconfSession(ConfigSource):
+ def __init__(self, session_env=None):
+ super().__init__()
+
+ if session_env:
+ self.__session_env = session_env
+ else:
+ self.__session_env = None
+
+ if session_env and 'CONFIGSESSION_PID' in session_env:
+ self.pid = int(session_env['CONFIGSESSION_PID'])
+ else:
+ self.pid = os.getppid()
+
+ self._vyconf_session = VyconfSession(pid=self.pid)
+ try:
+ out = self._vyconf_session.get_config()
+ except VyconfSessionError as e:
+ raise ConfigSourceError(f'Init error in {type(self)}: {e}')
+
+ session_dir = directories['vyconf_session_dir']
+
+ self.running_cache_path = os.path.join(session_dir, f'running_cache_{out}')
+ self.session_cache_path = os.path.join(session_dir, f'session_cache_{out}')
+
+ self._running_config = ConfigTree(internal=self.running_cache_path)
+ self._session_config = ConfigTree(internal=self.session_cache_path)
+
+ if os.path.isfile(self.running_cache_path):
+ os.remove(self.running_cache_path)
+ if os.path.isfile(self.session_cache_path):
+ os.remove(self.session_cache_path)
+
+ # N.B. level not yet implemented pending integration with legacy CLI
+ # cf. T7374
+ self._level = []
+
+ def __del__(self):
+ try:
+ if not self._vyconf_session.in_session():
+ self._vyconf_session.teardown()
+ except AttributeError:
+ pass
+
+ def get_level(self):
+ return self._level
+
+ def set_level(self):
+ pass
+
+ def session_changed(self):
+ """
+ Returns:
+ True if the config session has uncommited changes, False otherwise.
+ """
+ try:
+ return self._vyconf_session.session_changed()
+ except VyconfSessionError:
+ # no actionable session info on error
+ return False
+
+ def in_session(self):
+ """
+ Returns:
+ True if called from a configuration session, False otherwise.
+ """
+ return self._vyconf_session.in_session()
+
+ def show_config(self, path: Union[str,list] = None, default: str = None,
+ effective: bool = False):
+ """
+ Args:
+ path (str|list): Configuration tree path, or empty
+ default (str): Default value to return
+
+ Returns:
+ str: working configuration
+ """
+
+ if path is None:
+ path = []
+ if isinstance(path, str):
+ path = path.split()
+
+ ct = self._running_config if effective else self._session_config
+ with_node = True if self.is_tag(path) else False
+ ct_at_path = ct.get_subtree(path, with_node=with_node) if path else ct
+
+ res = ct_at_path.to_string().strip()
+
+ return res if res else default
+
+ def is_tag(self, path):
+ try:
+ return is_tag(path)
+ except ValueError:
+ return False
+
+ def is_leaf(self, path):
+ try:
+ return is_leaf(path)
+ except ValueError:
+ return False
+
+ def is_multi(self, path):
+ try:
+ return is_multi(path)
+ except ValueError:
+ return False
+
class ConfigSourceString(ConfigSource):
def __init__(self, running_config_text=None, session_config_text=None):
super().__init__()
@@ -319,3 +436,13 @@ class ConfigSourceString(ConfigSource):
self._session_config = ConfigTree(session_config_text) if session_config_text else None
except ValueError:
raise ConfigSourceError(f"Init error in {type(self)}")
+
+class ConfigSourceCache(ConfigSource):
+ def __init__(self, running_config_cache=None, session_config_cache=None):
+ super().__init__()
+
+ try:
+ self._running_config = ConfigTree(internal=running_config_cache) if running_config_cache else None
+ self._session_config = ConfigTree(internal=session_config_cache) if session_config_cache else None
+ except ValueError:
+ raise ConfigSourceError(f"Init error in {type(self)}")
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 8d27a7e46..aeeb329c5 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -1,5 +1,5 @@
# configtree -- a standalone VyOS config file manipulation library (Python bindings)
-# Copyright (C) 2018-2025 VyOS maintainers and contributors
+# Copyright 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;
@@ -19,7 +19,9 @@ import logging
from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool
-LIBPATH = '/usr/lib/libvyosconfig.so.0'
+BUILD_PATH = '/tmp/libvyosconfig/_build/libvyosconfig.so'
+INSTALL_PATH = '/usr/lib/libvyosconfig.so.0'
+LIBPATH = BUILD_PATH if os.path.isfile(BUILD_PATH) else INSTALL_PATH
def replace_backslash(s, search, replace):
@@ -48,7 +50,7 @@ def unescape_backslash(string: str) -> str:
def extract_version(s):
"""Extract the version string from the config string"""
t = re.split('(^//)', s, maxsplit=1, flags=re.MULTILINE)
- return (s, ''.join(t[1:]))
+ return (t[0], ''.join(t[1:]))
def check_path(path):
@@ -64,9 +66,14 @@ class ConfigTreeError(Exception):
class ConfigTree(object):
- def __init__(self, config_string=None, address=None, libpath=LIBPATH):
- if config_string is None and address is None:
- raise TypeError("ConfigTree() requires one of 'config_string' or 'address'")
+ def __init__(
+ self, config_string=None, address=None, internal=None, libpath=LIBPATH
+ ):
+ if config_string is None and address is None and internal is None:
+ raise TypeError(
+ "ConfigTree() requires one of 'config_string', 'address', or 'internal'"
+ )
+
self.__config = None
self.__lib = cdll.LoadLibrary(libpath)
@@ -87,6 +94,13 @@ class ConfigTree(object):
self.__to_commands.argtypes = [c_void_p, c_char_p]
self.__to_commands.restype = c_char_p
+ self.__read_internal = self.__lib.read_internal
+ self.__read_internal.argtypes = [c_char_p]
+ self.__read_internal.restype = c_void_p
+
+ self.__write_internal = self.__lib.write_internal
+ self.__write_internal.argtypes = [c_void_p, c_char_p]
+
self.__to_json = self.__lib.to_json
self.__to_json.argtypes = [c_void_p]
self.__to_json.restype = c_char_p
@@ -166,7 +180,22 @@ class ConfigTree(object):
self.__destroy = self.__lib.destroy
self.__destroy.argtypes = [c_void_p]
- if address is None:
+ self.__equal = self.__lib.equal
+ self.__equal.argtypes = [c_void_p, c_void_p]
+ self.__equal.restype = c_bool
+
+ if address is not None:
+ self.__config = address
+ self.__version = ''
+ elif internal is not None:
+ config = self.__read_internal(internal.encode())
+ if config is None:
+ msg = self.__get_error().decode()
+ raise ValueError('Failed to read internal rep: {0}'.format(msg))
+ else:
+ self.__config = config
+ self.__version = ''
+ elif config_string is not None:
config_section, version_section = extract_version(config_string)
config_section = escape_backslash(config_section)
config = self.__from_string(config_section.encode())
@@ -177,8 +206,9 @@ class ConfigTree(object):
self.__config = config
self.__version = version_section
else:
- self.__config = address
- self.__version = ''
+ raise TypeError(
+ "ConfigTree() requires one of 'config_string', 'address', or 'internal'"
+ )
self.__migration = os.environ.get('VYOS_MIGRATION')
if self.__migration:
@@ -188,6 +218,11 @@ class ConfigTree(object):
if self.__config is not None:
self.__destroy(self.__config)
+ def __eq__(self, other):
+ if isinstance(other, ConfigTree):
+ return self.__equal(self._get_config(), other._get_config())
+ return False
+
def __str__(self):
return self.to_string()
@@ -197,6 +232,9 @@ class ConfigTree(object):
def get_version_string(self):
return self.__version
+ def write_cache(self, file_name):
+ self.__write_internal(self._get_config(), file_name.encode())
+
def to_string(self, ordered_values=False, no_version=False):
config_string = self.__to_string(self.__config, ordered_values).decode()
config_string = unescape_backslash(config_string)
@@ -461,6 +499,28 @@ def union(left, right, libpath=LIBPATH):
return tree
+def merge(left, right, destructive=False, libpath=LIBPATH):
+ if left is None:
+ left = ConfigTree(config_string='\n')
+ if right is None:
+ right = ConfigTree(config_string='\n')
+ if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
+ raise TypeError('Arguments must be instances of ConfigTree')
+
+ __lib = cdll.LoadLibrary(libpath)
+ __tree_merge = __lib.tree_merge
+ __tree_merge.argtypes = [c_bool, c_void_p, c_void_p]
+ __tree_merge.restype = c_void_p
+ __get_error = __lib.get_error
+ __get_error.argtypes = []
+ __get_error.restype = c_char_p
+
+ res = __tree_merge(destructive, left._get_config(), right._get_config())
+ tree = ConfigTree(address=res)
+
+ return tree
+
+
def mask_inclusive(left, right, libpath=LIBPATH):
if not (isinstance(left, ConfigTree) and isinstance(right, ConfigTree)):
raise TypeError('Arguments must be instances of ConfigTree')
@@ -554,6 +614,44 @@ def reference_tree_cache_to_json(cache_path, render_file, libpath=LIBPATH):
raise ConfigTreeError(msg)
+# validate_tree_filter c_ptr rt_cache validator_dir
+def validate_tree_filter(
+ config_tree,
+ cache_path='/usr/share/vyos/reftree.cache',
+ validator_dir='/usr/libexec/vyos/validators',
+ libpath=LIBPATH,
+):
+ try:
+ __lib = cdll.LoadLibrary(libpath)
+ __validate_tree_filter = __lib.validate_tree_filter
+ __validate_tree_filter.argtypes = [c_void_p, c_char_p, c_char_p]
+ __get_error = __lib.get_error
+ __get_error.argtypes = []
+ __get_error.restype = c_char_p
+ res = __validate_tree_filter(
+ config_tree._get_config(), cache_path.encode(), validator_dir.encode()
+ )
+ except Exception as e:
+ raise ConfigTreeError(e)
+
+ msg = __get_error().decode()
+ tree = ConfigTree(address=res)
+
+ return tree, msg
+
+
+def validate_tree(
+ config_tree,
+ cache_path='/usr/share/vyos/reftree.cache',
+ validator_dir='/usr/libexec/vyos/validators',
+):
+ _, out = validate_tree_filter(
+ config_tree, cache_path=cache_path, validator_dir=validator_dir
+ )
+
+ return out
+
+
class DiffTree:
def __init__(self, left, right, path=[], libpath=LIBPATH):
if left is None:
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 4084425b1..cc4419913 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -92,6 +92,9 @@ def verify_mtu_ipv6(config):
tmp = dict_search('ipv6.address.eui64', config)
if tmp != None: raise ConfigError(error_msg)
+ tmp = dict_search('ipv6.address.interface_identifier', config)
+ if tmp != None: raise ConfigError(error_msg)
+
def verify_vrf(config):
"""
Common helper function used by interface implementations to perform
@@ -356,6 +359,7 @@ def verify_vlan_config(config):
verify_vrf(vlan)
verify_mirror_redirect(vlan)
verify_mtu_parent(vlan, config)
+ verify_mtu_ipv6(vlan)
# 802.1ad (Q-in-Q) VLANs
for s_vlan_id in config.get('vif_s', {}):
@@ -367,6 +371,7 @@ def verify_vlan_config(config):
verify_vrf(s_vlan)
verify_mirror_redirect(s_vlan)
verify_mtu_parent(s_vlan, config)
+ verify_mtu_ipv6(s_vlan)
for c_vlan_id in s_vlan.get('vif_c', {}):
c_vlan = s_vlan['vif_c'][c_vlan_id]
@@ -378,6 +383,7 @@ def verify_vlan_config(config):
verify_mirror_redirect(c_vlan)
verify_mtu_parent(c_vlan, config)
verify_mtu_parent(c_vlan, s_vlan)
+ verify_mtu_ipv6(c_vlan)
def verify_diffie_hellman_length(file, min_keysize):
@@ -521,6 +527,25 @@ def verify_pki_dh_parameters(config: dict, dh_name: str, min_key_size: int=0):
if dh_bits < min_key_size:
raise ConfigError(f'Minimum DH key-size is {min_key_size} bits!')
+def verify_pki_openssh_key(config: dict, key_name: str):
+ """
+ Common helper function user by PKI consumers to perform recurring
+ validation functions on OpenSSH keys
+ """
+ if 'pki' not in config:
+ raise ConfigError('PKI is not configured!')
+
+ if 'openssh' not in config['pki']:
+ raise ConfigError('PKI does not contain any OpenSSH keys!')
+
+ if key_name not in config['pki']['openssh']:
+ raise ConfigError(f'OpenSSH key "{key_name}" not found in configuration!')
+
+ if 'public' in config['pki']['openssh'][key_name]:
+ if not {'key', 'type'} <= set(config['pki']['openssh'][key_name]['public']):
+ raise ConfigError('Both public key and type must be defined for '\
+ f'OpenSSH public key "{key_name}"!')
+
def verify_eapol(config: dict):
"""
Common helper function used by interface implementations to perform
diff --git a/python/vyos/debug.py b/python/vyos/debug.py
index 6ce42b173..5b6e8172e 100644
--- a/python/vyos/debug.py
+++ b/python/vyos/debug.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9757a34df..5e3ba4e63 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -15,10 +15,10 @@
import os
-base_dir = '/usr/libexec/vyos/'
+base_dir = '/usr/libexec/vyos'
directories = {
- 'base' : base_dir,
+ 'base' : f'{base_dir}',
'data' : '/usr/share/vyos/',
'conf_mode' : f'{base_dir}/conf_mode',
'op_mode' : f'{base_dir}/op_mode',
@@ -37,7 +37,24 @@ directories = {
'dhcp6_client_dir' : '/run/dhcp6c',
'vyos_configdir' : '/opt/vyatta/config',
'completion_dir' : f'{base_dir}/completion',
- 'ca_certificates' : '/usr/local/share/ca-certificates/vyos'
+ 'ca_certificates' : '/usr/local/share/ca-certificates/vyos',
+ 'ppp_nexthop_dir' : '/run/ppp_nexthop',
+ 'proto_path' : '/usr/share/vyos/vyconf',
+ 'vyconf_session_dir' : f'{base_dir}/vyconf/session'
+}
+
+systemd_services = {
+ 'haproxy' : 'haproxy.service',
+ 'syslog' : 'syslog.service',
+ 'snmpd' : 'snmpd.service',
+}
+
+internal_ports = {
+ 'certbot_haproxy' : 65080, # Certbot running behing haproxy
+}
+
+config_files = {
+ 'sshd_user_ca' : '/run/sshd/trusted_user_ca',
}
config_status = '/tmp/vyos-config-status'
@@ -56,10 +73,20 @@ config_default = os.path.join(directories['data'], 'config.boot.default')
rt_symbolic_names = {
# Standard routing tables for Linux & reserved IDs for VyOS
- 'default': 253, # Confusingly, a final fallthru, not the default.
- 'main': 254, # The actual global table used by iproute2 unless told otherwise.
+ 'default': 253, # Confusingly, a final fallthru, not the default.
+ 'main': 254, # The actual global table used by iproute2 unless told otherwise.
'local': 255, # Special kernel loopback table.
}
rt_global_vrf = rt_symbolic_names['main']
rt_global_table = rt_symbolic_names['main']
+
+vyconfd_conf = '/etc/vyos/vyconfd.conf'
+
+DEFAULT_COMMIT_CONFIRM_MINUTES = 10
+
+commit_hooks = {'pre': '/etc/commit/pre-hooks.d',
+ 'post': '/etc/commit/post-hooks.d'
+ }
+
+airbag_noteworthy_size = 20
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index 4710a5d40..6c362163c 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -1,4 +1,4 @@
-# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 314e8dfe3..b136b6fca 100755
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -233,6 +233,9 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
hook_name = 'prerouting'
if hook == 'NAM':
hook_name = f'name'
+ # for policy
+ if hook == 'route' or hook == 'route6':
+ hook_name = hook
output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC{def_suffix}_{hook_name}_{fw_name}_{rule_id}')
if 'mac_address' in side_conf:
@@ -310,6 +313,16 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {prefix}addr {operator} @D_{group_name}')
+ elif 'remote_group' in group:
+ group_name = group['remote_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ if ip_name == 'ip':
+ output.append(f'{ip_name} {prefix}addr {operator} @R_{group_name}')
+ elif ip_name == 'ip6':
+ output.append(f'{ip_name} {prefix}addr {operator} @R6_{group_name}')
if 'mac_group' in group:
group_name = group['mac_group']
operator = ''
@@ -348,7 +361,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if iiface[0] == '!':
operator = '!='
iiface = iiface[1:]
- output.append(f'iifname {operator} {{{iiface}}}')
+ output.append(f'iifname {operator} {{"{iiface}"}}')
elif 'group' in rule_conf['inbound_interface']:
iiface = rule_conf['inbound_interface']['group']
if iiface[0] == '!':
@@ -363,7 +376,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if oiface[0] == '!':
operator = '!='
oiface = oiface[1:]
- output.append(f'oifname {operator} {{{oiface}}}')
+ output.append(f'oifname {operator} {{"{oiface}"}}')
elif 'group' in rule_conf['outbound_interface']:
oiface = rule_conf['outbound_interface']['group']
if oiface[0] == '!':
@@ -461,14 +474,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
output.append('gre version 1')
if gre_key:
- # The offset of the key within the packet shifts depending on the C-flag.
- # nftables cannot handle complex enough expressions to match multiple
+ # The offset of the key within the packet shifts depending on the C-flag.
+ # nftables cannot handle complex enough expressions to match multiple
# offsets based on bitfields elsewhere.
- # We enforce a specific match for the checksum flag in validation, so the
- # gre_flags dict will always have a 'checksum' key when gre_key is populated.
- if not gre_flags['checksum']:
+ # We enforce a specific match for the checksum flag in validation, so the
+ # gre_flags dict will always have a 'checksum' key when gre_key is populated.
+ if not gre_flags['checksum']:
# No "unset" child node means C is set, we offset key lookup +32 bits
- output.append(f'@th,64,32 == {gre_key}')
+ output.append(f'@th,64,32 == {gre_key}')
else:
output.append(f'@th,32,32 == {gre_key}')
@@ -627,7 +640,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
return " ".join(output)
def parse_gre_flags(flags, force_keyed=False):
- flag_map = { # nft does not have symbolic names for these.
+ flag_map = { # nft does not have symbolic names for these.
'checksum': 1<<0,
'routing': 1<<1,
'key': 1<<2,
@@ -638,7 +651,7 @@ def parse_gre_flags(flags, force_keyed=False):
include = 0
exclude = 0
for fl_name, fl_state in flags.items():
- if not fl_state:
+ if not fl_state:
include |= flag_map[fl_name]
else: # 'unset' child tag
exclude |= flag_map[fl_name]
@@ -731,14 +744,14 @@ class GeoIPLock(object):
def __exit__(self, exc_type, exc_value, tb):
os.unlink(self.file)
-def geoip_update(firewall, force=False):
+def geoip_update(firewall=None, policy=None, force=False):
with GeoIPLock(geoip_lock_file) as lock:
if not lock:
print("Script is already running")
return False
- if not firewall:
- print("Firewall is not configured")
+ if not firewall and not policy:
+ print("Firewall and policy are not configured")
return True
if not os.path.exists(geoip_database):
@@ -753,23 +766,41 @@ def geoip_update(firewall, force=False):
ipv4_sets = {}
ipv6_sets = {}
+ ipv4_codes_policy = {}
+ ipv6_codes_policy = {}
+
+ ipv4_sets_policy = {}
+ ipv6_sets_policy = {}
+
# Map country codes to set names
- for codes, path in dict_search_recursive(firewall, 'country_code'):
- set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
- if ( path[0] == 'ipv4'):
- for code in codes:
- ipv4_codes.setdefault(code, []).append(set_name)
- elif ( path[0] == 'ipv6' ):
- set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}'
- for code in codes:
- ipv6_codes.setdefault(code, []).append(set_name)
-
- if not ipv4_codes and not ipv6_codes:
+ if firewall:
+ for codes, path in dict_search_recursive(firewall, 'country_code'):
+ set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
+ if ( path[0] == 'ipv4'):
+ for code in codes:
+ ipv4_codes.setdefault(code, []).append(set_name)
+ elif ( path[0] == 'ipv6' ):
+ set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}'
+ for code in codes:
+ ipv6_codes.setdefault(code, []).append(set_name)
+
+ if policy:
+ for codes, path in dict_search_recursive(policy, 'country_code'):
+ set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}'
+ if ( path[0] == 'route'):
+ for code in codes:
+ ipv4_codes_policy.setdefault(code, []).append(set_name)
+ elif ( path[0] == 'route6' ):
+ set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}'
+ for code in codes:
+ ipv6_codes_policy.setdefault(code, []).append(set_name)
+
+ if not ipv4_codes and not ipv6_codes and not ipv4_codes_policy and not ipv6_codes_policy:
if force:
- print("GeoIP not in use by firewall")
+ print("GeoIP not in use by firewall and policy")
return True
- geoip_data = geoip_load_data([*ipv4_codes, *ipv6_codes])
+ geoip_data = geoip_load_data([*ipv4_codes, *ipv6_codes, *ipv4_codes_policy, *ipv6_codes_policy])
# Iterate IP blocks to assign to sets
for start, end, code in geoip_data:
@@ -778,19 +809,29 @@ def geoip_update(firewall, force=False):
ip_range = f'{start}-{end}' if start != end else start
for setname in ipv4_codes[code]:
ipv4_sets.setdefault(setname, []).append(ip_range)
+ if code in ipv4_codes_policy and ipv4:
+ ip_range = f'{start}-{end}' if start != end else start
+ for setname in ipv4_codes_policy[code]:
+ ipv4_sets_policy.setdefault(setname, []).append(ip_range)
if code in ipv6_codes and not ipv4:
ip_range = f'{start}-{end}' if start != end else start
for setname in ipv6_codes[code]:
ipv6_sets.setdefault(setname, []).append(ip_range)
+ if code in ipv6_codes_policy and not ipv4:
+ ip_range = f'{start}-{end}' if start != end else start
+ for setname in ipv6_codes_policy[code]:
+ ipv6_sets_policy.setdefault(setname, []).append(ip_range)
render(nftables_geoip_conf, 'firewall/nftables-geoip-update.j2', {
'ipv4_sets': ipv4_sets,
- 'ipv6_sets': ipv6_sets
+ 'ipv6_sets': ipv6_sets,
+ 'ipv4_sets_policy': ipv4_sets_policy,
+ 'ipv6_sets_policy': ipv6_sets_policy,
})
result = run(f'nft --file {nftables_geoip_conf}')
if result != 0:
- print('Error: GeoIP failed to update firewall')
+ print('Error: GeoIP failed to update firewall/policy')
return False
return True
diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py
index ba44978d1..f4ed69205 100644
--- a/python/vyos/frrender.py
+++ b/python/vyos/frrender.py
@@ -1,4 +1,4 @@
-# Copyright 2024-2025 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -60,6 +60,10 @@ def get_frrender_dict(conf, argv=None) -> dict:
from vyos.configdict import get_dhcp_interfaces
from vyos.configdict import get_pppoe_interfaces
+ # We need to re-set the CLI path to the root level, as this function uses
+ # conf.exists() with an absolute path form the CLI root
+ conf.set_level([])
+
# Create an empty dictionary which will be filled down the code path and
# returned to the caller
dict = {}
@@ -88,7 +92,7 @@ def get_frrender_dict(conf, argv=None) -> dict:
if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None:
del default_values['area'][area_num]['area_type']['nssa']
- for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
+ for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static']:
if dict_search(f'redistribute.{protocol}', ospf) is None:
del default_values['redistribute'][protocol]
if not bool(default_values['redistribute']):
@@ -539,6 +543,21 @@ def get_frrender_dict(conf, argv=None) -> dict:
elif conf.exists_effective(ospfv3_vrf_path):
vrf['name'][vrf_name]['protocols'].update({'ospfv3' : {'deleted' : ''}})
+ # We need to check the CLI if the RPKI node is present and thus load in all the default
+ # values present on the CLI - that's why we have if conf.exists()
+ rpki_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'rpki']
+ if 'rpki' in vrf_config.get('protocols', []):
+ rpki = conf.get_config_dict(rpki_vrf_path, key_mangling=('-', '_'), get_first_key=True,
+ with_pki=True, with_recursive_defaults=True)
+ rpki_ssh_key_base = '/run/frr/id_rpki'
+ for cache, cache_config in rpki.get('cache',{}).items():
+ if 'ssh' in cache_config:
+ cache_config['ssh']['public_key_file'] = f'{rpki_ssh_key_base}_{cache}.pub'
+ cache_config['ssh']['private_key_file'] = f'{rpki_ssh_key_base}_{cache}'
+ vrf['name'][vrf_name]['protocols'].update({'rpki' : rpki})
+ elif conf.exists_effective(rpki_vrf_path):
+ vrf['name'][vrf_name]['protocols'].update({'rpki' : {'deleted' : ''}})
+
# We need to check the CLI if the static node is present and thus load in all the default
# values present on the CLI - that's why we have if conf.exists()
static_vrf_path = ['vrf', 'name', vrf_name, 'protocols', 'static']
@@ -599,8 +618,10 @@ def get_frrender_dict(conf, argv=None) -> dict:
dict.update({'vrf' : vrf})
if os.path.exists(frr_debug_enable):
+ print(f'---- get_frrender_dict({conf}) ----')
import pprint
pprint.pprint(dict)
+ print('-----------------------------------')
return dict
@@ -669,7 +690,7 @@ class FRRender:
output += render_to_string('frr/ripngd.frr.j2', config_dict['ripng'])
output += '\n'
if 'rpki' in config_dict and 'deleted' not in config_dict['rpki']:
- output += render_to_string('frr/rpki.frr.j2', config_dict['rpki'])
+ output += render_to_string('frr/rpki.frr.j2', {'rpki': config_dict['rpki']})
output += '\n'
if 'segment_routing' in config_dict and 'deleted' not in config_dict['segment_routing']:
output += render_to_string('frr/zebra.segment_routing.frr.j2', config_dict['segment_routing'])
@@ -691,6 +712,9 @@ class FRRender:
debug('FRR: START CONFIGURATION RENDERING')
# we can not reload an empty file, thus we always embed the marker
output = '!\n'
+ # Enable FRR logging
+ output += 'log syslog\n'
+ output += 'log facility local7\n'
# Enable SNMP agentx support
# SNMP AgentX support cannot be disabled once enabled
if 'snmp' in config_dict:
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
index 206b2bba1..7838fa9a2 100644
--- a/python/vyos/ifconfig/__init__.py
+++ b/python/vyos/ifconfig/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/afi.py b/python/vyos/ifconfig/afi.py
index fd263d220..a391cb8a0 100644
--- a/python/vyos/ifconfig/afi.py
+++ b/python/vyos/ifconfig/afi.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py
index a659b9bd2..ba31ae0c9 100644
--- a/python/vyos/ifconfig/bond.py
+++ b/python/vyos/ifconfig/bond.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -485,11 +485,25 @@ class BondIf(Interface):
# Add (enslave) interfaces to bond
value = dict_search('member.interface', config)
+ is_first = True
for interface in (value or []):
- # if we've come here we already verified the interface
- # does not have an addresses configured so just flush
- # any remaining ones
- Interface(interface).flush_addrs()
+ # Physical bond member interface instance
+ tmp_if = Interface(interface)
+ # At this point, we've confirmed that the interface has no
+ # configured addresses, so we can safely flush any remaining ones.
+ tmp_if.flush_addrs()
+
+ # T7571: This behavior changed from Linux Kernel 5.4 (used in VyOS 1.3)
+ # to Kernel 6.6 (starting with VyOS 1.4). Previously, the MAC address of
+ # the first member interface in a bond was adopted as the bond's default MAC.
+ # In newer versions, a synthetic MAC address is assigned instead.
+ #
+ # Re-assign first underlay MAC address to the bond
+ if is_first:
+ self.set_mac(tmp_if.get_mac())
+ is_first = False
+
+ # Assign underlaying interface to logical bond
self.add_port(interface)
# Add system mac address for 802.3ad - default address is all zero
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index d534dade7..ba06e3757 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -19,7 +19,7 @@ from vyos.utils.assertion import assert_list
from vyos.utils.assertion import assert_positive
from vyos.utils.dict import dict_search
from vyos.utils.network import interface_exists
-from vyos.configdict import get_vlan_ids
+from vyos.configdict import get_vlans_ids_and_range
from vyos.configdict import list_diff
@Interface.register
@@ -376,11 +376,21 @@ class BridgeIf(Interface):
if 'priority' in interface_config:
lower.set_path_priority(interface_config['priority'])
+ # set BPDU guard
+ tmp = dict_search('bpdu_guard', interface_config)
+ value = '1' if (tmp != None) else '0'
+ lower.set_bpdu_guard(value)
+
+ # set root guard
+ tmp = dict_search('root_guard', interface_config)
+ value = '1' if (tmp != None) else '0'
+ lower.set_root_guard(value)
+
if 'enable_vlan' in config:
add_vlan = []
native_vlan_id = None
allowed_vlan_ids= []
- cur_vlan_ids = get_vlan_ids(interface)
+ cur_vlan_ids = get_vlans_ids_and_range(interface)
if 'native_vlan' in interface_config:
vlan_id = interface_config['native_vlan']
@@ -389,14 +399,8 @@ class BridgeIf(Interface):
if 'allowed_vlan' in interface_config:
for vlan in interface_config['allowed_vlan']:
- vlan_range = vlan.split('-')
- if len(vlan_range) == 2:
- for vlan_add in range(int(vlan_range[0]),int(vlan_range[1]) + 1):
- add_vlan.append(str(vlan_add))
- allowed_vlan_ids.append(str(vlan_add))
- else:
- add_vlan.append(vlan)
- allowed_vlan_ids.append(vlan)
+ add_vlan.append(vlan)
+ allowed_vlan_ids.append(vlan)
# Remove redundant VLANs from the system
for vlan in list_diff(cur_vlan_ids, add_vlan):
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
index 7402da55a..3cf3297b4 100644
--- a/python/vyos/ifconfig/control.py
+++ b/python/vyos/ifconfig/control.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -29,6 +29,8 @@ class Control(Section):
_command_get = {}
_command_set = {}
_signature = {}
+ config = {}
+ ifname = None
def __init__(self, **kargs):
# some commands (such as operation comands - show interfaces, etc.)
@@ -48,7 +50,7 @@ class Control(Section):
def _popen(self, command):
return popen(command, self.debug)
- def _cmd(self, command):
+ def _cmd(self, command, env=None):
import re
if 'netns' in self.config:
# This command must be executed from default netns 'ip link set dev X netns X'
@@ -61,7 +63,7 @@ class Control(Section):
command = command
else:
command = f'ip netns exec {self.config["netns"]} {command}'
- return cmd(command, self.debug)
+ return cmd(command, self.debug, env=env)
def _get_command(self, config, name):
"""
diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py
index 29a1965a3..93066c965 100644
--- a/python/vyos/ifconfig/dummy.py
+++ b/python/vyos/ifconfig/dummy.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 93727bdf6..864a9d0bc 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py
index f7fddb812..7c5b7c0fb 100644
--- a/python/vyos/ifconfig/geneve.py
+++ b/python/vyos/ifconfig/geneve.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -48,7 +48,7 @@ class GeneveIf(Interface):
'parameters.ipv6.flowlabel' : 'flowlabel',
}
- cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote}'
+ cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote} dstport {port}'
for vyos_key, iproute2_key in mapping.items():
# dict_search will return an empty dict "{}" for valueless nodes like
# "parameters.nolearning" - thus we need to test the nodes existence
diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py
index 201d3cacb..6cb1eb64c 100644
--- a/python/vyos/ifconfig/input.py
+++ b/python/vyos/ifconfig/input.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index cb73e2597..3b3536301 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2025 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -22,10 +22,10 @@ from copy import deepcopy
from glob import glob
from ipaddress import IPv4Network
-from netifaces import ifaddresses
-# this is not the same as socket.AF_INET/INET6
-from netifaces import AF_INET
-from netifaces import AF_INET6
+from ipaddress import IPv6Interface
+from netifaces import ifaddresses # pylint: disable = no-name-in-module
+from socket import AF_INET
+from socket import AF_INET6
from netaddr import EUI
from netaddr import mac_unix_expanded
@@ -216,6 +216,16 @@ class Interface(Control):
'location': '/sys/class/net/{ifname}/brport/priority',
'errormsg': '{ifname} is not a bridge port member'
},
+ 'bpdu_guard': {
+ 'validate': assert_boolean,
+ 'location': '/sys/class/net/{ifname}/brport/bpdu_guard',
+ 'errormsg': '{ifname} is not a bridge port member'
+ },
+ 'root_guard': {
+ 'validate': assert_boolean,
+ 'location': '/sys/class/net/{ifname}/brport/root_block',
+ 'errormsg': '{ifname} is not a bridge port member'
+ },
'proxy_arp': {
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp',
@@ -412,11 +422,11 @@ class Interface(Control):
self._cmd(f'nft {nft_command}')
def _del_interface_from_ct_iface_map(self):
- nft_command = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}'
+ nft_command = f'delete element inet vrf_zones ct_iface_map {{ \'"{self.ifname}"\' }}'
self._nft_check_and_run(nft_command)
def _add_interface_to_ct_iface_map(self, vrf_table_id: int):
- nft_command = f'add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}'
+ nft_command = f'add element inet vrf_zones ct_iface_map {{ \'"{self.ifname}"\' : {vrf_table_id} }}'
self._nft_check_and_run(nft_command)
def get_ifindex(self):
@@ -595,12 +605,16 @@ class Interface(Control):
"""
Add/Remove interface from given VRF instance.
+ Keyword arguments:
+ vrf: VRF instance name or empty string (default VRF)
+
+ Return True if VRF was changed, False otherwise
+
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_vrf('foo')
>>> Interface('eth0').set_vrf()
"""
-
# Don't allow for netns yet
if 'netns' in self.config:
return False
@@ -611,21 +625,33 @@ class Interface(Control):
# Get current VRF table ID
old_vrf_tableid = get_vrf_tableid(self.ifname)
- self.set_interface('vrf', vrf)
+ # Always stop the DHCP client process to clean up routes within the VRF
+ # where the process was originally started. There is no need to add a
+ # condition to only call the method if "address dhcp" was defined, as
+ # this is handled inside set_dhcp(v6) by only stopping if the daemon is
+ # running. DHCP client process restart will be handled later on once the
+ # interface is moved to the new VRF.
+ self.set_dhcp(False)
+ self.set_dhcpv6(False)
+
+ # Move interface in/out of VRF
+ self.set_interface('vrf', vrf)
if vrf:
# Get routing table ID number for VRF
vrf_table_id = get_vrf_tableid(vrf)
# Add map element with interface and zone ID
- if vrf_table_id:
+ if vrf_table_id and old_vrf_tableid != vrf_table_id:
# delete old table ID from nftables if it has changed, e.g. interface moved to a different VRF
- if old_vrf_tableid and old_vrf_tableid != int(vrf_table_id):
- self._del_interface_from_ct_iface_map()
+ self._del_interface_from_ct_iface_map()
self._add_interface_to_ct_iface_map(vrf_table_id)
+ return True
else:
- self._del_interface_from_ct_iface_map()
+ if old_vrf_tableid != get_vrf_tableid(self.ifname):
+ self._del_interface_from_ct_iface_map()
+ return True
- return True
+ return False
def set_arp_cache_tmo(self, tmo):
"""
@@ -893,7 +919,11 @@ class Interface(Control):
tmp = self.get_interface('ipv6_autoconf')
if tmp == autoconf:
return None
- return self.set_interface('ipv6_autoconf', autoconf)
+ rc = self.set_interface('ipv6_autoconf', autoconf)
+ if autoconf == '0':
+ flushed = self.flush_ipv6_slaac_addrs()
+ self.flush_ipv6_slaac_routes(ra_addrs=flushed)
+ return rc
def add_ipv6_eui64_address(self, prefix):
"""
@@ -921,6 +951,20 @@ class Interface(Control):
prefixlen = prefix.split('/')[1]
self.del_addr(f'{eui64}/{prefixlen}')
+ def set_ipv6_interface_identifier(self, identifier):
+ """
+ Set the interface identifier for IPv6 autoconf.
+ """
+ cmd = f'ip token set {identifier} dev {self.ifname}'
+ self._cmd(cmd)
+
+ def del_ipv6_interface_identifier(self):
+ """
+ Delete the interface identifier for IPv6 autoconf.
+ """
+ cmd = f'ip token delete dev {self.ifname}'
+ self._cmd(cmd)
+
def set_ipv6_forwarding(self, forwarding):
"""
Configure IPv6 interface-specific Host/Router behaviour.
@@ -1071,6 +1115,28 @@ class Interface(Control):
"""
self.set_interface('path_priority', priority)
+ def set_bpdu_guard(self, state):
+ """
+ Set BPDU guard state for a bridge port. When enabled, the port will be
+ disabled if it receives a BPDU packet.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_bpdu_guard(1)
+ """
+ self.set_interface('bpdu_guard', state)
+
+ def set_root_guard(self, state):
+ """
+ Set root guard state for a bridge port. When enabled, the port will be
+ disabled if it receives a superior BPDU that would make it a root port.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_root_guard(1)
+ """
+ self.set_interface('root_guard', state)
+
def set_port_isolation(self, on_or_off):
"""
Controls whether a given port will be isolated, which means it will be
@@ -1181,7 +1247,7 @@ class Interface(Control):
"""
return self.get_addr_v4() + self.get_addr_v6()
- def add_addr(self, addr):
+ def add_addr(self, addr: str, vrf_changed: bool=False) -> bool:
"""
Add IP(v6) address to interface. Address is only added if it is not
already assigned to that interface. Address format must be validated
@@ -1214,15 +1280,14 @@ class Interface(Control):
# add to interface
if addr == 'dhcp':
- self.set_dhcp(True)
+ self.set_dhcp(True, vrf_changed=vrf_changed)
elif addr == 'dhcpv6':
- self.set_dhcpv6(True)
+ self.set_dhcpv6(True, vrf_changed=vrf_changed)
elif not is_intf_addr_assigned(self.ifname, addr, netns=netns):
netns_cmd = f'ip netns exec {netns}' if netns else ''
tmp = f'{netns_cmd} ip addr add {addr} dev {self.ifname}'
# Add broadcast address for IPv4
if is_ipv4(addr): tmp += ' brd +'
-
self._cmd(tmp)
else:
return False
@@ -1232,7 +1297,7 @@ class Interface(Control):
return True
- def del_addr(self, addr):
+ def del_addr(self, addr: str) -> bool:
"""
Delete IP(v6) address from interface. Address is only deleted if it is
assigned to that interface. Address format must be exactly the same as
@@ -1295,6 +1360,71 @@ class Interface(Control):
# flush all addresses
self._cmd(cmd)
+ def flush_ipv6_slaac_addrs(self) -> list:
+ """
+ Flush all IPv6 addresses installed in response to router advertisement
+ messages from this interface.
+
+ Will raise an exception on error.
+ Will return a list of flushed IPv6 addresses.
+ """
+ netns = get_interface_namespace(self.ifname)
+ netns_cmd = f'ip netns exec {netns}' if netns else ''
+ tmp = get_interface_address(self.ifname)
+ if not tmp or 'addr_info' not in tmp:
+ return
+
+ # Parse interface IP addresses. Example data:
+ # {'family': 'inet6', 'local': '2001:db8:1111:0:250:56ff:feb3:38c5',
+ # 'prefixlen': 64, 'scope': 'global', 'dynamic': True,
+ # 'mngtmpaddr': True, 'protocol': 'kernel_ra',
+ # 'valid_life_time': 2591987, 'preferred_life_time': 14387}
+ flushed = []
+ for addr_info in tmp['addr_info']:
+ if 'protocol' not in addr_info:
+ continue
+ if (addr_info['protocol'] == 'kernel_ra' and
+ addr_info['scope'] == 'global'):
+ # Flush IPv6 addresses installed by router advertisement
+ ra_addr = f"{addr_info['local']}/{addr_info['prefixlen']}"
+ flushed.append(ra_addr)
+ cmd = f'{netns_cmd} ip -6 addr del dev {self.ifname} {ra_addr}'
+ self._cmd(cmd)
+ return flushed
+
+ def flush_ipv6_slaac_routes(self, ra_addrs: list=[]) -> None:
+ """
+ Flush IPv6 default routes installed in response to router advertisement
+ messages from this interface.
+
+ Will raise an exception on error.
+ """
+ # Find IPv6 connected prefixes for flushed SLAAC addresses
+ connected = []
+ for addr in ra_addrs if isinstance(ra_addrs, list) else []:
+ connected.append(str(IPv6Interface(addr).network))
+
+ netns = get_interface_namespace(self.ifname)
+ netns_cmd = f'ip netns exec {netns}' if netns else ''
+
+ tmp = self._cmd(f'{netns_cmd} ip -j -6 route show dev {self.ifname}')
+ tmp = json.loads(tmp)
+ # Parse interface routes. Example data:
+ # {'dst': 'default', 'gateway': 'fe80::250:56ff:feb3:cdba',
+ # 'protocol': 'ra', 'metric': 1024, 'flags': [], 'expires': 1398,
+ # 'metrics': [{'hoplimit': 64}], 'pref': 'medium'}
+ for route in tmp:
+ # If it's a default route received from RA, delete it
+ if (dict_search('dst', route) == 'default' and
+ dict_search('protocol', route) == 'ra'):
+ self._cmd(f'{netns_cmd} ip -6 route del default via {route["gateway"]} dev {self.ifname}')
+ # Remove connected prefixes received from RA
+ if dict_search('dst', route) in connected:
+ # If it's a connected prefix, delete it
+ self._cmd(f'{netns_cmd} ip -6 route del {route["dst"]} dev {self.ifname}')
+
+ return None
+
def add_to_bridge(self, bridge_dict):
"""
Adds the interface to the bridge with the passed port config.
@@ -1305,8 +1435,6 @@ class Interface(Control):
# drop all interface addresses first
self.flush_addrs()
- ifname = self.ifname
-
for bridge, bridge_config in bridge_dict.items():
# add interface to bridge - use Section.klass to get BridgeIf class
Section.klass(bridge)(bridge, create=True).add_port(self.ifname)
@@ -1322,7 +1450,7 @@ class Interface(Control):
bridge_vlan_filter = Section.klass(bridge)(bridge, create=True).get_vlan_filter()
if int(bridge_vlan_filter):
- cur_vlan_ids = get_vlan_ids(ifname)
+ cur_vlan_ids = get_vlan_ids(self.ifname)
add_vlan = []
native_vlan_id = None
allowed_vlan_ids= []
@@ -1345,18 +1473,18 @@ class Interface(Control):
# Remove redundant VLANs from the system
for vlan in list_diff(cur_vlan_ids, add_vlan):
- cmd = f'bridge vlan del dev {ifname} vid {vlan} master'
+ cmd = f'bridge vlan del dev {self.ifname} vid {vlan} master'
self._cmd(cmd)
for vlan in allowed_vlan_ids:
- cmd = f'bridge vlan add dev {ifname} vid {vlan} master'
+ cmd = f'bridge vlan add dev {self.ifname} vid {vlan} master'
self._cmd(cmd)
# Setting native VLAN to system
if native_vlan_id:
- cmd = f'bridge vlan add dev {ifname} vid {native_vlan_id} pvid untagged master'
+ cmd = f'bridge vlan add dev {self.ifname} vid {native_vlan_id} pvid untagged master'
self._cmd(cmd)
- def set_dhcp(self, enable):
+ def set_dhcp(self, enable: bool, vrf_changed: bool=False):
"""
Enable/Disable DHCP client on a given interface.
"""
@@ -1396,7 +1524,9 @@ class Interface(Control):
# the old lease is released a new one is acquired (T4203). We will
# only restart DHCP client if it's option changed, or if it's not
# running, but it should be running (e.g. on system startup)
- if 'dhcp_options_changed' in self.config or not is_systemd_service_active(systemd_service):
+ if (vrf_changed or
+ ('dhcp_options_changed' in self.config) or
+ (not is_systemd_service_active(systemd_service))):
return self._cmd(f'systemctl restart {systemd_service}')
else:
if is_systemd_service_active(systemd_service):
@@ -1423,19 +1553,18 @@ class Interface(Control):
return None
- def set_dhcpv6(self, enable):
+ def set_dhcpv6(self, enable: bool, vrf_changed: bool=False):
"""
Enable/Disable DHCPv6 client on a given interface.
"""
if enable not in [True, False]:
raise ValueError()
- ifname = self.ifname
config_base = directories['dhcp6_client_dir']
- config_file = f'{config_base}/dhcp6c.{ifname}.conf'
- script_file = f'/etc/wide-dhcpv6/dhcp6c.{ifname}.script' # can not live under /run b/c of noexec mount option
- systemd_override_file = f'/run/systemd/system/dhcp6c@{ifname}.service.d/10-override.conf'
- systemd_service = f'dhcp6c@{ifname}.service'
+ config_file = f'{config_base}/dhcp6c.{self.ifname}.conf'
+ script_file = f'/etc/wide-dhcpv6/dhcp6c.{self.ifname}.script' # can not live under /run b/c of noexec mount option
+ systemd_override_file = f'/run/systemd/system/dhcp6c@{self.ifname}.service.d/10-override.conf'
+ systemd_service = f'dhcp6c@{self.ifname}.service'
# Rendered client configuration files require additional settings
config = deepcopy(self.config)
@@ -1452,7 +1581,10 @@ class Interface(Control):
# We must ignore any return codes. This is required to enable
# DHCPv6-PD for interfaces which are yet not up and running.
- return self._popen(f'systemctl restart {systemd_service}')
+ if (vrf_changed or
+ ('dhcpv6_options_changed' in self.config) or
+ (not is_systemd_service_active(systemd_service))):
+ return self._popen(f'systemctl restart {systemd_service}')
else:
if is_systemd_service_active(systemd_service):
self._cmd(f'systemctl stop {systemd_service}')
@@ -1669,30 +1801,31 @@ class Interface(Control):
else:
self.del_addr(addr)
- # start DHCPv6 client when only PD was configured
- if dhcpv6pd:
- self.set_dhcpv6(True)
-
# XXX: Bind interface to given VRF or unbind it if vrf is not set. Unbinding
# will call 'ip link set dev eth0 nomaster' which will also drop the
# interface out of any bridge or bond - thus this is checked before.
+ vrf_changed = False
if 'is_bond_member' in config:
bond_if = next(iter(config['is_bond_member']))
tmp = get_interface_config(config['ifname'])
if 'master' in tmp and tmp['master'] != bond_if:
- self.set_vrf('')
+ vrf_changed = self.set_vrf('')
elif 'is_bridge_member' in config:
bridge_if = next(iter(config['is_bridge_member']))
tmp = get_interface_config(config['ifname'])
if 'master' in tmp and tmp['master'] != bridge_if:
- self.set_vrf('')
+ vrf_changed = self.set_vrf('')
else:
- self.set_vrf(config.get('vrf', ''))
+ vrf_changed = self.set_vrf(config.get('vrf', ''))
+
+ # start DHCPv6 client when only PD was configured
+ if dhcpv6pd:
+ self.set_dhcpv6(True, vrf_changed=vrf_changed)
# Add this section after vrf T4331
for addr in new_addr:
- self.add_addr(addr)
+ self.add_addr(addr, vrf_changed=vrf_changed)
# Configure MSS value for IPv4 TCP connections
tmp = dict_search('ip.adjust_mss', config)
@@ -1771,11 +1904,26 @@ class Interface(Control):
value = '0' if (tmp != None) else '1'
self.set_ipv6_forwarding(value)
+ # Delete old interface identifier
+ # This should be before setting the accept_ra value
+ old = dict_search('ipv6.address.interface_identifier_old', config)
+ now = dict_search('ipv6.address.interface_identifier', config)
+ if old and not now:
+ # accept_ra of ra is required to delete the interface identifier
+ self.set_ipv6_accept_ra('2')
+ self.del_ipv6_interface_identifier()
+
+ # Set IPv6 Interface identifier
+ # This should be before setting the accept_ra value
+ tmp = dict_search('ipv6.address.interface_identifier', config)
+ if tmp:
+ # accept_ra is required to set the interface identifier
+ self.set_ipv6_accept_ra('2')
+ self.set_ipv6_interface_identifier(tmp)
+
# IPv6 router advertisements
tmp = dict_search('ipv6.address.autoconf', config)
- value = '2' if (tmp != None) else '1'
- if 'dhcpv6' in new_addr:
- value = '2'
+ value = '2' if (tmp != None) else '0'
self.set_ipv6_accept_ra(value)
# IPv6 address autoconfiguration
diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py
index dfaa006aa..ea9294e99 100644
--- a/python/vyos/ifconfig/l2tpv3.py
+++ b/python/vyos/ifconfig/l2tpv3.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py
index 13e8a2c50..f4fc2c906 100644
--- a/python/vyos/ifconfig/loopback.py
+++ b/python/vyos/ifconfig/loopback.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py
index 3b4dc223f..4d76a1d46 100644
--- a/python/vyos/ifconfig/macsec.py
+++ b/python/vyos/ifconfig/macsec.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py
index fe948b920..7a26f9ef5 100644
--- a/python/vyos/ifconfig/macvlan.py
+++ b/python/vyos/ifconfig/macvlan.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py
index dc2742123..e60518948 100644
--- a/python/vyos/ifconfig/operational.py
+++ b/python/vyos/ifconfig/operational.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py
index 85ca3877e..4ca66cf4d 100644
--- a/python/vyos/ifconfig/pppoe.py
+++ b/python/vyos/ifconfig/pppoe.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py
index 50273cf67..4ea606495 100644
--- a/python/vyos/ifconfig/section.py
+++ b/python/vyos/ifconfig/section.py
@@ -1,4 +1,4 @@
-# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/sstpc.py b/python/vyos/ifconfig/sstpc.py
index d92ef23dc..e43a2f177 100644
--- a/python/vyos/ifconfig/sstpc.py
+++ b/python/vyos/ifconfig/sstpc.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py
index df904f7d5..f96364161 100644
--- a/python/vyos/ifconfig/tunnel.py
+++ b/python/vyos/ifconfig/tunnel.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/veth.py b/python/vyos/ifconfig/veth.py
index 2c8709d20..f4075fa02 100644
--- a/python/vyos/ifconfig/veth.py
+++ b/python/vyos/ifconfig/veth.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py
index 3ee22706c..4949fe571 100644
--- a/python/vyos/ifconfig/vrrp.py
+++ b/python/vyos/ifconfig/vrrp.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
index 78f5895f8..030aa1ed7 100644
--- a/python/vyos/ifconfig/vti.py
+++ b/python/vyos/ifconfig/vti.py
@@ -1,4 +1,4 @@
-# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py
index ee790f275..e6963ce5d 100644
--- a/python/vyos/ifconfig/vtun.py
+++ b/python/vyos/ifconfig/vtun.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
index 58844885b..0f55acf10 100644
--- a/python/vyos/ifconfig/vxlan.py
+++ b/python/vyos/ifconfig/vxlan.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index 519012625..c4e70056c 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -22,10 +22,13 @@ from tempfile import NamedTemporaryFile
from hurry.filesize import size
from hurry.filesize import alternative
+from vyos.base import Warning
+from vyos.configquery import ConfigTreeQuery
from vyos.ifconfig import Interface
from vyos.ifconfig import Operational
from vyos.template import is_ipv6
-
+from vyos.template import is_ipv4
+from vyos.utils.network import get_wireguard_peers
class WireGuardOperational(Operational):
def _dump(self):
"""Dump wireguard data in a python friendly way."""
@@ -49,7 +52,7 @@ class WireGuardOperational(Operational):
'private_key': None if private_key == '(none)' else private_key,
'public_key': None if public_key == '(none)' else public_key,
'listen_port': int(listen_port),
- 'fw_mark': None if fw_mark == 'off' else int(fw_mark),
+ 'fw_mark': None if fw_mark == 'off' else int(fw_mark, 16),
'peers': {},
}
else:
@@ -90,12 +93,15 @@ class WireGuardOperational(Operational):
c.set_level(['interfaces', 'wireguard', self.config['ifname']])
description = c.return_effective_value(['description'])
ips = c.return_effective_values(['address'])
+ hostnames = c.return_effective_values(['host-name'])
answer = 'interface: {}\n'.format(self.config['ifname'])
if description:
answer += ' description: {}\n'.format(description)
if ips:
answer += ' address: {}\n'.format(', '.join(ips))
+ if hostnames:
+ answer += ' hostname: {}\n'.format(', '.join(hostnames))
answer += ' public key: {}\n'.format(wgdump['public_key'])
answer += ' private key: (hidden)\n'
@@ -155,6 +161,77 @@ class WireGuardOperational(Operational):
answer += '\n'
return answer
+ def get_latest_handshakes(self):
+ """Get latest handshake time for each peer"""
+ output = {}
+
+ # Dump wireguard last handshake
+ tmp = self._cmd(f'wg show {self.ifname} latest-handshakes')
+ # Output:
+ # PUBLIC-KEY= 1732812147
+ for line in tmp.split('\n'):
+ if not line:
+ # Skip empty lines and last line
+ continue
+ items = line.split('\t')
+
+ if len(items) != 2:
+ continue
+
+ output[items[0]] = int(items[1])
+
+ return output
+
+ def reset_peer(self, peer_name=None, public_key=None):
+ c = ConfigTreeQuery()
+ tmp = c.get_config_dict(['interfaces', 'wireguard', self.ifname],
+ effective=True, get_first_key=True,
+ key_mangling=('-', '_'), with_defaults=True)
+
+ current_peers = self._dump().get(self.ifname, {}).get('peers', {})
+
+ for peer, peer_config in tmp['peer'].items():
+ peer_public_key = peer_config['public_key']
+ if peer_name is None or peer == peer_name or public_key == peer_public_key:
+ if ('address' not in peer_config and 'host_name' not in peer_config) or 'port' not in peer_config:
+ if peer_name is not None:
+ print(f'WireGuard interface "{self.ifname}" peer "{peer_name}" address/host-name unset!')
+ continue
+
+ # As we work with an effective config, a port CLI node is always
+ # available when an address/host-name is defined on the CLI
+ port = peer_config['port']
+
+ # address has higher priority than host-name
+ if 'address' in peer_config:
+ address = peer_config['address']
+ new_endpoint = f'{address}:{port}'
+ else:
+ host_name = peer_config['host_name']
+ new_endpoint = f'{host_name}:{port}'
+
+ if 'disable' in peer_config:
+ print(f'WireGuard interface "{self.ifname}" peer "{peer_name}" disabled!')
+ continue
+
+ cmd = f'wg set {self.ifname} peer {peer_public_key} endpoint {new_endpoint}'
+ try:
+ if (peer_public_key in current_peers
+ and 'endpoint' in current_peers[peer_public_key]
+ and current_peers[peer_public_key]['endpoint'] is not None
+ ):
+ current_endpoint = current_peers[peer_public_key]['endpoint']
+ message = f'Resetting {self.ifname} peer {peer_public_key} from {current_endpoint} endpoint to {new_endpoint} ... '
+ else:
+ message = f'Resetting {self.ifname} peer {peer_public_key} endpoint to {new_endpoint} ... '
+ print(message, end='')
+
+ self._cmd(cmd, env={'WG_ENDPOINT_RESOLUTION_RETRIES':
+ tmp['max_dns_retry']})
+ print('done')
+ except:
+ print(f'Error\nPlease try to run command manually:\n{cmd}\n')
+
@Interface.register
class WireGuardIf(Interface):
@@ -175,29 +252,66 @@ class WireGuardIf(Interface):
"""Get a synthetic MAC address."""
return self.get_mac_synthetic()
+ def get_peer_public_keys(self, config, disabled=False):
+ """Get list of configured peer public keys"""
+ if 'peer' not in config:
+ return []
+
+ public_keys = []
+
+ for _, peer_config in config['peer'].items():
+ if disabled == ('disable' in peer_config):
+ public_keys.append(peer_config['public_key'])
+
+ return public_keys
+
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
interface setup code and provide a single point of entry when workin
on any interface."""
- tmp_file = NamedTemporaryFile('w')
- tmp_file.write(config['private_key'])
- tmp_file.flush()
-
# Wireguard base command is identical for every peer
- base_cmd = 'wg set {ifname}'
+ base_cmd = f'wg set {self.ifname}'
+
+ interface_cmd = base_cmd
if 'port' in config:
- base_cmd += ' listen-port {port}'
+ interface_cmd += ' listen-port {port}'
if 'fwmark' in config:
- base_cmd += ' fwmark {fwmark}'
+ interface_cmd += ' fwmark {fwmark}'
+
+ with NamedTemporaryFile('w') as tmp_file:
+ tmp_file.write(config['private_key'])
+ tmp_file.flush()
+
+ interface_cmd += f' private-key {tmp_file.name}'
+ interface_cmd = interface_cmd.format(**config)
+ # T6490: execute command to ensure interface configured
+ self._cmd(interface_cmd)
+
+ current_peer_public_keys = get_wireguard_peers(self.ifname)
- base_cmd += f' private-key {tmp_file.name}'
- base_cmd = base_cmd.format(**config)
- # T6490: execute command to ensure interface configured
- self._cmd(base_cmd)
+ if 'rebuild_required' in config:
+ # Remove all existing peers that no longer exist in config
+ current_public_keys = self.get_peer_public_keys(config)
+ cmd_remove_peers = [f' peer {public_key} remove'
+ for public_key in current_peer_public_keys
+ if public_key not in current_public_keys]
+ if cmd_remove_peers:
+ self._cmd(base_cmd + ''.join(cmd_remove_peers))
if 'peer' in config:
+ # Group removal of disabled peers in one command
+ current_disabled_peers = self.get_peer_public_keys(config, disabled=True)
+ cmd_disabled_peers = [f' peer {public_key} remove'
+ for public_key in current_disabled_peers]
+ if cmd_disabled_peers:
+ self._cmd(base_cmd + ''.join(cmd_disabled_peers))
+
+ peer_cmds = []
+ peer_domain_cmds = []
+ peer_psk_files = []
+
for peer, peer_config in config['peer'].items():
# T4702: No need to configure this peer when it was explicitly
# marked as disabled - also active sessions are terminated as
@@ -206,17 +320,20 @@ class WireGuardIf(Interface):
continue
# start of with a fresh 'wg' command
- cmd = base_cmd + ' peer {public_key}'
+ peer_cmd = ' peer {public_key}'
+
+ cmd = peer_cmd
- # If no PSK is given remove it by using /dev/null - passing keys via
- # the shell (usually bash) is considered insecure, thus we use a file
- no_psk_file = '/dev/null'
- psk_file = no_psk_file
if 'preshared_key' in peer_config:
- psk_file = '/tmp/tmp.wireguard.psk'
- with open(psk_file, 'w') as f:
- f.write(peer_config['preshared_key'])
- cmd += f' preshared-key {psk_file}'
+ with NamedTemporaryFile(mode='w', delete=False) as tmp_file:
+ tmp_file.write(peer_config['preshared_key'])
+ tmp_file.flush()
+ cmd += f' preshared-key {tmp_file.name}'
+ peer_psk_files.append(tmp_file.name)
+ else:
+ # If no PSK is given remove it by using /dev/null - passing keys via
+ # the shell (usually bash) is considered insecure, thus we use a file
+ cmd += f' preshared-key /dev/null'
# Persistent keepalive is optional
if 'persistent_keepalive' in peer_config:
@@ -228,18 +345,38 @@ class WireGuardIf(Interface):
peer_config['allowed_ips'] = [peer_config['allowed_ips']]
cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips'])
- # Endpoint configuration is optional
+ peer_cmds.append(cmd.format(**peer_config))
+
+ cmd = peer_cmd
+
+ # Ensure peer is created even if dns not working
if {'address', 'port'} <= set(peer_config):
if is_ipv6(peer_config['address']):
cmd += ' endpoint [{address}]:{port}'
- else:
+ elif is_ipv4(peer_config['address']):
cmd += ' endpoint {address}:{port}'
+ else:
+ # don't set endpoint if address uses domain name
+ continue
+ elif {'host_name', 'port'} <= set(peer_config):
+ cmd += ' endpoint {host_name}:{port}'
+ else:
+ continue
+
+ peer_domain_cmds.append(cmd.format(**peer_config))
- self._cmd(cmd.format(**peer_config))
+ try:
+ if peer_cmds:
+ self._cmd(base_cmd + ''.join(peer_cmds))
- # PSK key file is not required to be stored persistently as its backed by CLI
- if psk_file != no_psk_file and os.path.exists(psk_file):
- os.remove(psk_file)
+ if peer_domain_cmds:
+ self._cmd(base_cmd + ''.join(peer_domain_cmds), env={
+ 'WG_ENDPOINT_RESOLUTION_RETRIES': config['max_dns_retry']})
+ except Exception as e:
+ Warning(f'Failed to apply Wireguard peers on {self.ifname}: {e}')
+ finally:
+ for tmp in peer_psk_files:
+ os.unlink(tmp)
# call base class
super().update(config)
diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py
index 121f56bd5..69fd87347 100644
--- a/python/vyos/ifconfig/wireless.py
+++ b/python/vyos/ifconfig/wireless.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ifconfig/wwan.py b/python/vyos/ifconfig/wwan.py
index 004a64b39..2b5714b85 100644
--- a/python/vyos/ifconfig/wwan.py
+++ b/python/vyos/ifconfig/wwan.py
@@ -1,4 +1,4 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/iflag.py b/python/vyos/iflag.py
index 3ce73c1bf..179f33497 100644
--- a/python/vyos/iflag.py
+++ b/python/vyos/iflag.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/include/__init__.py b/python/vyos/include/__init__.py
new file mode 100644
index 000000000..ba196ffed
--- /dev/null
+++ b/python/vyos/include/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 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/>.
+
diff --git a/python/vyos/include/uapi/__init__.py b/python/vyos/include/uapi/__init__.py
new file mode 100644
index 000000000..ba196ffed
--- /dev/null
+++ b/python/vyos/include/uapi/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 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/>.
+
diff --git a/python/vyos/include/uapi/linux/__init__.py b/python/vyos/include/uapi/linux/__init__.py
new file mode 100644
index 000000000..ba196ffed
--- /dev/null
+++ b/python/vyos/include/uapi/linux/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 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/>.
+
diff --git a/python/vyos/include/uapi/linux/fib_rules.py b/python/vyos/include/uapi/linux/fib_rules.py
new file mode 100644
index 000000000..83544f69b
--- /dev/null
+++ b/python/vyos/include/uapi/linux/fib_rules.py
@@ -0,0 +1,20 @@
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+FIB_RULE_PERMANENT = 0x00000001
+FIB_RULE_INVERT = 0x00000002
+FIB_RULE_UNRESOLVED = 0x00000004
+FIB_RULE_IIF_DETACHED = 0x00000008
+FIB_RULE_DEV_DETACHED = FIB_RULE_IIF_DETACHED
+FIB_RULE_OIF_DETACHED = 0x00000010
diff --git a/python/vyos/include/uapi/linux/icmpv6.py b/python/vyos/include/uapi/linux/icmpv6.py
new file mode 100644
index 000000000..cc30b76fd
--- /dev/null
+++ b/python/vyos/include/uapi/linux/icmpv6.py
@@ -0,0 +1,18 @@
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+ICMPV6_ROUTER_PREF_LOW = 3
+ICMPV6_ROUTER_PREF_MEDIUM = 0
+ICMPV6_ROUTER_PREF_HIGH = 1
+ICMPV6_ROUTER_PREF_INVALID = 2
diff --git a/python/vyos/include/uapi/linux/if_arp.py b/python/vyos/include/uapi/linux/if_arp.py
new file mode 100644
index 000000000..80c16a83d
--- /dev/null
+++ b/python/vyos/include/uapi/linux/if_arp.py
@@ -0,0 +1,176 @@
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# ARP protocol HARDWARE identifiers
+ARPHRD_NETROM = 0 # from KA9Q: NET/ROM pseudo
+ARPHRD_ETHER = 1 # Ethernet 10Mbps
+ARPHRD_EETHER = 2 # Experimental Ethernet
+ARPHRD_AX25 = 3 # AX.25 Level 2
+ARPHRD_PRONET = 4 # PROnet token ring
+ARPHRD_CHAOS = 5 # Chaosnet
+ARPHRD_IEEE802 = 6 # IEEE 802.2 Ethernet/TR/TB
+ARPHRD_ARCNET = 7 # ARCnet
+ARPHRD_APPLETLK = 8 # APPLEtalk
+ARPHRD_DLCI = 15 # Frame Relay DLCI
+ARPHRD_ATM = 19 # ATM
+ARPHRD_METRICOM = 23 # Metricom STRIP (new IANA id)
+ARPHRD_IEEE1394 = 24 # IEEE 1394 IPv4 - RFC 2734
+ARPHRD_EUI64 = 27 # EUI-64
+ARPHRD_INFINIBAND = 32 # InfiniBand
+
+# Dummy types for non-ARP hardware
+ARPHRD_SLIP = 256
+ARPHRD_CSLIP = 257
+ARPHRD_SLIP6 = 258
+ARPHRD_CSLIP6 = 259
+ARPHRD_RSRVD = 260 # Notional KISS type
+ARPHRD_ADAPT = 264
+ARPHRD_ROSE = 270
+ARPHRD_X25 = 271 # CCITT X.25
+ARPHRD_HWX25 = 272 # Boards with X.25 in firmware
+ARPHRD_CAN = 280 # Controller Area Network
+ARPHRD_MCTP = 290
+ARPHRD_PPP = 512
+ARPHRD_CISCO = 513 # Cisco HDLC
+ARPHRD_HDLC = ARPHRD_CISCO # Alias for CISCO
+ARPHRD_LAPB = 516 # LAPB
+ARPHRD_DDCMP = 517 # Digital's DDCMP protocol
+ARPHRD_RAWHDLC = 518 # Raw HDLC
+ARPHRD_RAWIP = 519 # Raw IP
+
+ARPHRD_TUNNEL = 768 # IPIP tunnel
+ARPHRD_TUNNEL6 = 769 # IP6IP6 tunnel
+ARPHRD_FRAD = 770 # Frame Relay Access Device
+ARPHRD_SKIP = 771 # SKIP vif
+ARPHRD_LOOPBACK = 772 # Loopback device
+ARPHRD_LOCALTLK = 773 # Localtalk device
+ARPHRD_FDDI = 774 # Fiber Distributed Data Interface
+ARPHRD_BIF = 775 # AP1000 BIF
+ARPHRD_SIT = 776 # sit0 device - IPv6-in-IPv4
+ARPHRD_IPDDP = 777 # IP over DDP tunneller
+ARPHRD_IPGRE = 778 # GRE over IP
+ARPHRD_PIMREG = 779 # PIMSM register interface
+ARPHRD_HIPPI = 780 # High Performance Parallel Interface
+ARPHRD_ASH = 781 # Nexus 64Mbps Ash
+ARPHRD_ECONET = 782 # Acorn Econet
+ARPHRD_IRDA = 783 # Linux-IrDA
+ARPHRD_FCPP = 784 # Point to point fibrechannel
+ARPHRD_FCAL = 785 # Fibrechannel arbitrated loop
+ARPHRD_FCPL = 786 # Fibrechannel public loop
+ARPHRD_FCFABRIC = 787 # Fibrechannel fabric
+
+ARPHRD_IEEE802_TR = 800 # Magic type ident for TR
+ARPHRD_IEEE80211 = 801 # IEEE 802.11
+ARPHRD_IEEE80211_PRISM = 802 # IEEE 802.11 + Prism2 header
+ARPHRD_IEEE80211_RADIOTAP = 803 # IEEE 802.11 + radiotap header
+ARPHRD_IEEE802154 = 804
+ARPHRD_IEEE802154_MONITOR = 805 # IEEE 802.15.4 network monitor
+
+ARPHRD_PHONET = 820 # PhoNet media type
+ARPHRD_PHONET_PIPE = 821 # PhoNet pipe header
+ARPHRD_CAIF = 822 # CAIF media type
+ARPHRD_IP6GRE = 823 # GRE over IPv6
+ARPHRD_NETLINK = 824 # Netlink header
+ARPHRD_6LOWPAN = 825 # IPv6 over LoWPAN
+ARPHRD_VSOCKMON = 826 # Vsock monitor header
+
+ARPHRD_VOID = 0xFFFF # Void type, nothing is known
+ARPHRD_NONE = 0xFFFE # Zero header length
+
+# ARP protocol opcodes
+ARPOP_REQUEST = 1 # ARP request
+ARPOP_REPLY = 2 # ARP reply
+ARPOP_RREQUEST = 3 # RARP request
+ARPOP_RREPLY = 4 # RARP reply
+ARPOP_InREQUEST = 8 # InARP request
+ARPOP_InREPLY = 9 # InARP reply
+ARPOP_NAK = 10 # (ATM)ARP NAK
+
+ARPHRD_TO_NAME = {
+ ARPHRD_NETROM: "netrom",
+ ARPHRD_ETHER: "ether",
+ ARPHRD_EETHER: "eether",
+ ARPHRD_AX25: "ax25",
+ ARPHRD_PRONET: "pronet",
+ ARPHRD_CHAOS: "chaos",
+ ARPHRD_IEEE802: "ieee802",
+ ARPHRD_ARCNET: "arcnet",
+ ARPHRD_APPLETLK: "atalk",
+ ARPHRD_DLCI: "dlci",
+ ARPHRD_ATM: "atm",
+ ARPHRD_METRICOM: "metricom",
+ ARPHRD_IEEE1394: "ieee1394",
+ ARPHRD_INFINIBAND: "infiniband",
+ ARPHRD_SLIP: "slip",
+ ARPHRD_CSLIP: "cslip",
+ ARPHRD_SLIP6: "slip6",
+ ARPHRD_CSLIP6: "cslip6",
+ ARPHRD_RSRVD: "rsrvd",
+ ARPHRD_ADAPT: "adapt",
+ ARPHRD_ROSE: "rose",
+ ARPHRD_X25: "x25",
+ ARPHRD_HWX25: "hwx25",
+ ARPHRD_CAN: "can",
+ ARPHRD_PPP: "ppp",
+ ARPHRD_HDLC: "hdlc",
+ ARPHRD_LAPB: "lapb",
+ ARPHRD_DDCMP: "ddcmp",
+ ARPHRD_RAWHDLC: "rawhdlc",
+ ARPHRD_TUNNEL: "ipip",
+ ARPHRD_TUNNEL6: "tunnel6",
+ ARPHRD_FRAD: "frad",
+ ARPHRD_SKIP: "skip",
+ ARPHRD_LOOPBACK: "loopback",
+ ARPHRD_LOCALTLK: "ltalk",
+ ARPHRD_FDDI: "fddi",
+ ARPHRD_BIF: "bif",
+ ARPHRD_SIT: "sit",
+ ARPHRD_IPDDP: "ip/ddp",
+ ARPHRD_IPGRE: "gre",
+ ARPHRD_PIMREG: "pimreg",
+ ARPHRD_HIPPI: "hippi",
+ ARPHRD_ASH: "ash",
+ ARPHRD_ECONET: "econet",
+ ARPHRD_IRDA: "irda",
+ ARPHRD_FCPP: "fcpp",
+ ARPHRD_FCAL: "fcal",
+ ARPHRD_FCPL: "fcpl",
+ ARPHRD_FCFABRIC: "fcfb0",
+ ARPHRD_FCFABRIC+1: "fcfb1",
+ ARPHRD_FCFABRIC+2: "fcfb2",
+ ARPHRD_FCFABRIC+3: "fcfb3",
+ ARPHRD_FCFABRIC+4: "fcfb4",
+ ARPHRD_FCFABRIC+5: "fcfb5",
+ ARPHRD_FCFABRIC+6: "fcfb6",
+ ARPHRD_FCFABRIC+7: "fcfb7",
+ ARPHRD_FCFABRIC+8: "fcfb8",
+ ARPHRD_FCFABRIC+9: "fcfb9",
+ ARPHRD_FCFABRIC+10: "fcfb10",
+ ARPHRD_FCFABRIC+11: "fcfb11",
+ ARPHRD_FCFABRIC+12: "fcfb12",
+ ARPHRD_IEEE802_TR: "tr",
+ ARPHRD_IEEE80211: "ieee802.11",
+ ARPHRD_IEEE80211_PRISM: "ieee802.11/prism",
+ ARPHRD_IEEE80211_RADIOTAP: "ieee802.11/radiotap",
+ ARPHRD_IEEE802154: "ieee802.15.4",
+ ARPHRD_IEEE802154_MONITOR: "ieee802.15.4/monitor",
+ ARPHRD_PHONET: "phonet",
+ ARPHRD_PHONET_PIPE: "phonet_pipe",
+ ARPHRD_CAIF: "caif",
+ ARPHRD_IP6GRE: "gre6",
+ ARPHRD_NETLINK: "netlink",
+ ARPHRD_6LOWPAN: "6lowpan",
+ ARPHRD_NONE: "none",
+ ARPHRD_VOID: "void",
+} \ No newline at end of file
diff --git a/python/vyos/include/uapi/linux/lwtunnel.py b/python/vyos/include/uapi/linux/lwtunnel.py
new file mode 100644
index 000000000..c598513a5
--- /dev/null
+++ b/python/vyos/include/uapi/linux/lwtunnel.py
@@ -0,0 +1,38 @@
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+LWTUNNEL_ENCAP_NONE = 0
+LWTUNNEL_ENCAP_MPLS = 1
+LWTUNNEL_ENCAP_IP = 2
+LWTUNNEL_ENCAP_ILA = 3
+LWTUNNEL_ENCAP_IP6 = 4
+LWTUNNEL_ENCAP_SEG6 = 5
+LWTUNNEL_ENCAP_BPF = 6
+LWTUNNEL_ENCAP_SEG6_LOCAL = 7
+LWTUNNEL_ENCAP_RPL = 8
+LWTUNNEL_ENCAP_IOAM6 = 9
+LWTUNNEL_ENCAP_XFRM = 10
+
+ENCAP_TO_NAME = {
+ LWTUNNEL_ENCAP_MPLS: 'mpls',
+ LWTUNNEL_ENCAP_IP: 'ip',
+ LWTUNNEL_ENCAP_IP6: 'ip6',
+ LWTUNNEL_ENCAP_ILA: 'ila',
+ LWTUNNEL_ENCAP_BPF: 'bpf',
+ LWTUNNEL_ENCAP_SEG6: 'seg6',
+ LWTUNNEL_ENCAP_SEG6_LOCAL: 'seg6local',
+ LWTUNNEL_ENCAP_RPL: 'rpl',
+ LWTUNNEL_ENCAP_IOAM6: 'ioam6',
+ LWTUNNEL_ENCAP_XFRM: 'xfrm',
+}
diff --git a/python/vyos/include/uapi/linux/neighbour.py b/python/vyos/include/uapi/linux/neighbour.py
new file mode 100644
index 000000000..8878353e3
--- /dev/null
+++ b/python/vyos/include/uapi/linux/neighbour.py
@@ -0,0 +1,34 @@
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+NTF_USE = (1 << 0)
+NTF_SELF = (1 << 1)
+NTF_MASTER = (1 << 2)
+NTF_PROXY = (1 << 3)
+NTF_EXT_LEARNED = (1 << 4)
+NTF_OFFLOADED = (1 << 5)
+NTF_STICKY = (1 << 6)
+NTF_ROUTER = (1 << 7)
+NTF_EXT_MANAGED = (1 << 0)
+NTF_EXT_LOCKED = (1 << 1)
+
+NTF_FlAGS = {
+ 'self': NTF_SELF,
+ 'router': NTF_ROUTER,
+ 'extern_learn': NTF_EXT_LEARNED,
+ 'offload': NTF_OFFLOADED,
+ 'master': NTF_MASTER,
+ 'sticky': NTF_STICKY,
+ 'locked': NTF_EXT_LOCKED,
+}
diff --git a/python/vyos/include/uapi/linux/rtnetlink.py b/python/vyos/include/uapi/linux/rtnetlink.py
new file mode 100644
index 000000000..f3778fa65
--- /dev/null
+++ b/python/vyos/include/uapi/linux/rtnetlink.py
@@ -0,0 +1,63 @@
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+RTM_F_NOTIFY = 0x100
+RTM_F_CLONED = 0x200
+RTM_F_EQUALIZE = 0x400
+RTM_F_PREFIX = 0x800
+RTM_F_LOOKUP_TABLE = 0x1000
+RTM_F_FIB_MATCH = 0x2000
+RTM_F_OFFLOAD = 0x4000
+RTM_F_TRAP = 0x8000
+RTM_F_OFFLOAD_FAILED = 0x20000000
+
+RTNH_F_DEAD = 1
+RTNH_F_PERVASIVE = 2
+RTNH_F_ONLINK = 4
+RTNH_F_OFFLOAD = 8
+RTNH_F_LINKDOWN = 16
+RTNH_F_UNRESOLVED = 32
+RTNH_F_TRAP = 64
+
+RT_TABLE_COMPAT = 252
+RT_TABLE_DEFAULT = 253
+RT_TABLE_MAIN = 254
+RT_TABLE_LOCAL = 255
+
+RTAX_FEATURE_ECN = (1 << 0)
+RTAX_FEATURE_SACK = (1 << 1)
+RTAX_FEATURE_TIMESTAMP = (1 << 2)
+RTAX_FEATURE_ALLFRAG = (1 << 3)
+RTAX_FEATURE_TCP_USEC_TS = (1 << 4)
+
+RT_FlAGS = {
+ 'dead': RTNH_F_DEAD,
+ 'onlink': RTNH_F_ONLINK,
+ 'pervasive': RTNH_F_PERVASIVE,
+ 'offload': RTNH_F_OFFLOAD,
+ 'trap': RTNH_F_TRAP,
+ 'notify': RTM_F_NOTIFY,
+ 'linkdown': RTNH_F_LINKDOWN,
+ 'unresolved': RTNH_F_UNRESOLVED,
+ 'rt_offload': RTM_F_OFFLOAD,
+ 'rt_trap': RTM_F_TRAP,
+ 'rt_offload_failed': RTM_F_OFFLOAD_FAILED,
+}
+
+RT_TABLE_TO_NAME = {
+ RT_TABLE_COMPAT: 'compat',
+ RT_TABLE_DEFAULT: 'default',
+ RT_TABLE_MAIN: 'main',
+ RT_TABLE_LOCAL: 'local',
+}
diff --git a/python/vyos/initialsetup.py b/python/vyos/initialsetup.py
index cb6b9e459..bff3adf20 100644
--- a/python/vyos/initialsetup.py
+++ b/python/vyos/initialsetup.py
@@ -1,7 +1,7 @@
# initialsetup -- functions for setting common values in config file,
# for use in installation and first boot scripts
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright 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;
diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py
index 51574c1db..7f9ad226a 100644
--- a/python/vyos/ioctl.py
+++ b/python/vyos/ioctl.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/ipsec.py b/python/vyos/ipsec.py
index 28f77565a..81f3d0812 100644
--- a/python/vyos/ipsec.py
+++ b/python/vyos/ipsec.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 951c83693..a47371e88 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2025 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -20,8 +20,8 @@ import socket
from datetime import datetime
from datetime import timezone
+from vyos import ConfigError
from vyos.template import is_ipv6
-from vyos.template import isc_static_route
from vyos.template import netmask_from_cidr
from vyos.utils.dict import dict_search_args
from vyos.utils.file import file_permissions
@@ -44,6 +44,7 @@ kea4_options = {
'wpad_url': 'wpad-url',
'ipv6_only_preferred': 'v6-only-preferred',
'captive_portal': 'v4-captive-portal',
+ 'capwap_controller': 'capwap-ac-v4',
}
kea6_options = {
@@ -56,9 +57,10 @@ kea6_options = {
'nisplus_server': 'nisp-servers',
'sntp_server': 'sntp-servers',
'captive_portal': 'v6-captive-portal',
+ 'capwap_controller': 'capwap-ac-v6',
}
-kea_ctrl_socket = '/run/kea/dhcp{inet}-ctrl-socket'
+kea_ctrl_socket = '/run/kea/dhcp{inet}{vrf_append}-ctrl-socket'
def _format_hex_string(in_str):
@@ -111,22 +113,21 @@ def kea_parse_options(config):
default_route = ''
if 'default_router' in config:
- default_route = isc_static_route('0.0.0.0/0', config['default_router'])
+ default_route = f'0.0.0.0/0 - {config["default_router"]}'
routes = [
- isc_static_route(route, route_options['next_hop'])
+ f'{route} - {route_options["next_hop"]}'
for route, route_options in config['static_route'].items()
]
options.append(
{
- 'name': 'rfc3442-static-route',
+ 'name': 'classless-static-route',
'data': ', '.join(
routes if not default_route else routes + [default_route]
),
}
)
- options.append({'name': 'windows-static-route', 'data': ', '.join(routes)})
if 'time_zone' in config:
with open('/usr/share/zoneinfo/' + config['time_zone'], 'rb') as f:
@@ -147,7 +148,7 @@ def kea_parse_options(config):
def kea_parse_subnet(subnet, config):
- out = {'subnet': subnet, 'id': int(config['subnet_id'])}
+ out = {'subnet': subnet, 'id': int(config['subnet_id']), 'user-context': {}}
if 'option' in config:
out['option-data'] = kea_parse_options(config['option'])
@@ -165,6 +166,9 @@ def kea_parse_subnet(subnet, config):
out['valid-lifetime'] = int(config['lease'])
out['max-valid-lifetime'] = int(config['lease'])
+ if 'ping_check' in config:
+ out['user-context']['enable-ping-check'] = True
+
if 'range' in config:
pools = []
for num, range_config in config['range'].items():
@@ -218,6 +222,9 @@ def kea_parse_subnet(subnet, config):
reservations.append(reservation)
out['reservations'] = reservations
+ if 'dynamic_dns_update' in config:
+ out.update(kea_parse_ddns_settings(config['dynamic_dns_update']))
+
return out
@@ -347,9 +354,62 @@ def kea6_parse_subnet(subnet, config):
return out
+def kea_parse_tsig_algo(algo_spec):
+ translate = {
+ 'md5': 'HMAC-MD5',
+ 'sha1': 'HMAC-SHA1',
+ 'sha224': 'HMAC-SHA224',
+ 'sha256': 'HMAC-SHA256',
+ 'sha384': 'HMAC-SHA384',
+ 'sha512': 'HMAC-SHA512'
+ }
+ if algo_spec not in translate:
+ raise ConfigError(f'Unsupported TSIG algorithm: {algo_spec}')
+ return translate[algo_spec]
+
+def kea_parse_enable_disable(value):
+ return True if value == 'enable' else False
+
+def kea_parse_ddns_settings(config):
+ data = {}
+
+ if send_updates := config.get('send_updates'):
+ data['ddns-send-updates'] = kea_parse_enable_disable(send_updates)
+
+ if override_client_update := config.get('override_client_update'):
+ data['ddns-override-client-update'] = kea_parse_enable_disable(override_client_update)
+
+ if override_no_update := config.get('override_no_update'):
+ data['ddns-override-no-update'] = kea_parse_enable_disable(override_no_update)
+
+ if update_on_renew := config.get('update_on_renew'):
+ data['ddns-update-on-renew'] = kea_parse_enable_disable(update_on_renew)
+
+ if conflict_resolution := config.get('conflict_resolution'):
+ data['ddns-use-conflict-resolution'] = kea_parse_enable_disable(conflict_resolution)
+
+ if 'replace_client_name' in config:
+ data['ddns-replace-client-name'] = config['replace_client_name']
+ if 'generated_prefix' in config:
+ data['ddns-generated-prefix'] = config['generated_prefix']
+ if 'qualifying_suffix' in config:
+ data['ddns-qualifying-suffix'] = config['qualifying_suffix']
+ if 'ttl_percent' in config:
+ data['ddns-ttl-percent'] = int(config['ttl_percent']) / 100
+ if 'hostname_char_set' in config:
+ data['hostname-char-set'] = config['hostname_char_set']
+ if 'hostname_char_replacement' in config:
+ data['hostname-char-replacement'] = config['hostname_char_replacement']
-def _ctrl_socket_command(inet, command, args=None):
- path = kea_ctrl_socket.format(inet=inet)
+ return data
+
+def _ctrl_socket_command(inet, vrf_name, command, args=None):
+ if vrf_name:
+ vrf_append = f'-{vrf_name}'
+ else:
+ vrf_append = ''
+
+ path = kea_ctrl_socket.format(inet=inet, vrf_append=vrf_append)
if not os.path.exists(path):
return None
@@ -375,8 +435,8 @@ def _ctrl_socket_command(inet, command, args=None):
return json.loads(result.decode('utf-8'))
-def kea_get_leases(inet):
- leases = _ctrl_socket_command(inet, f'lease{inet}-get-all')
+def kea_get_leases(inet, vrf_name):
+ leases = _ctrl_socket_command(inet, vrf_name, f'lease{inet}-get-all')
if not leases or 'result' not in leases or leases['result'] != 0:
return []
@@ -384,10 +444,35 @@ def kea_get_leases(inet):
return leases['arguments']['leases']
-def kea_delete_lease(inet, ip_address):
+def kea_add_lease(
+ inet,
+ vrf_name,
+ ip_address,
+ host_name=None,
+ mac_address=None,
+ iaid=None,
+ duid=None,
+ subnet_id=None,
+):
args = {'ip-address': ip_address}
- result = _ctrl_socket_command(inet, f'lease{inet}-del', args)
+ if host_name:
+ args['hostname'] = host_name
+
+ if subnet_id:
+ args['subnet-id'] = subnet_id
+
+ # IPv4 requires MAC address, IPv6 requires either MAC address or DUID
+ if mac_address:
+ args['hw-address'] = mac_address
+ if duid:
+ args['duid'] = duid
+
+ # IPv6 requires IAID
+ if inet == '6' and iaid:
+ args['iaid'] = iaid
+
+ result = _ctrl_socket_command(inet, vrf_name, f'lease{inet}-add', args)
if result and 'result' in result:
return result['result'] == 0
@@ -395,8 +480,19 @@ def kea_delete_lease(inet, ip_address):
return False
-def kea_get_active_config(inet):
- config = _ctrl_socket_command(inet, 'config-get')
+def kea_delete_lease(inet, ip_address, vrf_name=''):
+ args = {'ip-address': ip_address}
+
+ result = _ctrl_socket_command(inet, vrf_name, f'lease{inet}-del', args)
+
+ if result and 'result' in result:
+ return result['result'] == 0
+
+ return False
+
+
+def kea_get_active_config(inet, vrf_name):
+ config = _ctrl_socket_command(inet, vrf_name, 'config-get')
if not config or 'result' not in config or config['result'] != 0:
return None
@@ -430,6 +526,32 @@ def kea_get_pool_from_subnet_id(config, inet, subnet_id):
return None
+def kea_get_domain_from_subnet_id(config, inet, subnet_id):
+ shared_networks = dict_search_args(
+ config, 'arguments', f'Dhcp{inet}', 'shared-networks'
+ )
+
+ if not shared_networks:
+ return None
+
+ for network in shared_networks:
+ if f'subnet{inet}' not in network:
+ continue
+
+ for subnet in network[f'subnet{inet}']:
+ if 'id' in subnet and int(subnet['id']) == int(subnet_id):
+ for option in subnet['option-data']:
+ if option['name'] == 'domain-name':
+ return option['data']
+
+ # domain-name is not found in subnet, fallback to shared-network pool option
+ for option in network['option-data']:
+ if option['name'] == 'domain-name':
+ return option['data']
+
+ return None
+
+
def kea_get_static_mappings(config, inet, pools=[]) -> list:
"""
Get DHCP static mapping from active Kea DHCPv4 or DHCPv6 configuration
@@ -464,22 +586,21 @@ def kea_get_static_mappings(config, inet, pools=[]) -> list:
return mappings
-def kea_get_server_leases(config, inet, pools=[], state=[], origin=None) -> list:
+def kea_get_server_leases(config, inet, vrf_name, pools=[], state=[], origin=None) -> list:
"""
Get DHCP server leases from active Kea DHCPv4 or DHCPv6 configuration
:return list
"""
- leases = kea_get_leases(inet)
+ leases = kea_get_leases(inet, vrf_name)
data = []
for lease in leases:
lifetime = lease['valid-lft']
- expiry = lease['cltt'] + lifetime
+ start = lease['cltt']
+ expiry = start + lifetime
- lease['start_timestamp'] = datetime.fromtimestamp(
- expiry - lifetime, timezone.utc
- )
- lease['expire_timestamp'] = (
+ lease['start_time'] = datetime.fromtimestamp(start, timezone.utc)
+ lease['expire_time'] = (
datetime.fromtimestamp(expiry, timezone.utc) if expiry else None
)
@@ -492,19 +613,24 @@ def kea_get_server_leases(config, inet, pools=[], state=[], origin=None) -> list
if config
else '-'
)
+ data_lease['domain'] = (
+ kea_get_domain_from_subnet_id(config, inet, lease['subnet-id'])
+ if config
+ else ''
+ )
data_lease['end'] = (
- lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None
+ lease['expire_time'].timestamp() if lease['expire_time'] else None
)
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('.')
+ data_lease['hostname'] = lease.get('hostname', '').rstrip('.') or '-'
if inet == '4':
data_lease['mac'] = lease['hw-address']
- data_lease['start'] = lease['start_timestamp'].timestamp()
+ data_lease['start'] = lease['start_time'].timestamp()
if inet == '6':
- data_lease['last_communication'] = lease['start_timestamp'].timestamp()
+ data_lease['last_communication'] = lease['start_time'].timestamp()
data_lease['duid'] = _format_hex_string(lease['duid'])
data_lease['type'] = lease['type']
@@ -512,21 +638,17 @@ def kea_get_server_leases(config, inet, pools=[], state=[], origin=None) -> list
prefix_len = lease['prefix-len']
data_lease['ip'] += f'/{prefix_len}'
- data_lease['remaining'] = '-'
-
- if lease['valid-lft'] > 0:
- data_lease['remaining'] = lease['expire_timestamp'] - datetime.now(
- timezone.utc
- )
+ data_lease['remaining'] = ''
- if data_lease['remaining'].days >= 0:
- # substraction gives us a timedelta object which can't be formatted with strftime
- # so we use str(), split gets rid of the microseconds
- data_lease['remaining'] = str(data_lease['remaining']).split('.')[0]
+ now = datetime.now(timezone.utc)
+ if lease['valid-lft'] > 0 and lease['expire_time'] > now:
+ # substraction gives us a timedelta object which can't be formatted
+ # with strftime so we use str(), split gets rid of the microseconds
+ data_lease['remaining'] = str(lease['expire_time'] - now).split('.')[0]
# Do not add old leases
if (
- data_lease['remaining']
+ data_lease['remaining'] != ''
and data_lease['pool'] in pools
and data_lease['state'] != 'free'
and (not state or state == 'all' or data_lease['state'] in state)
diff --git a/python/vyos/limericks.py b/python/vyos/limericks.py
index 3c6744816..0c02d5292 100644
--- a/python/vyos/limericks.py
+++ b/python/vyos/limericks.py
@@ -1,4 +1,4 @@
-# Copyright 2015, 2018 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/load_config.py b/python/vyos/load_config.py
index b910a2f92..f65e887f0 100644
--- a/python/vyos/load_config.py
+++ b/python/vyos/load_config.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/logger.py b/python/vyos/logger.py
index f7cc964d5..207f95c1b 100644
--- a/python/vyos/logger.py
+++ b/python/vyos/logger.py
@@ -1,4 +1,4 @@
-# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/migrate.py b/python/vyos/migrate.py
index 9d1613676..c06f6a76c 100644
--- a/python/vyos/migrate.py
+++ b/python/vyos/migrate.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/nat.py b/python/vyos/nat.py
index 29f8e961b..7be957a0c 100644
--- a/python/vyos/nat.py
+++ b/python/vyos/nat.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index 7b11d36dd..7f1fc6b4f 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/pki.py b/python/vyos/pki.py
index 55dc02631..4598c5daa 100644
--- a/python/vyos/pki.py
+++ b/python/vyos/pki.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
diff --git a/python/vyos/priority.py b/python/vyos/priority.py
index ab4e6d411..e61281d3c 100644
--- a/python/vyos/priority.py
+++ b/python/vyos/priority.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/progressbar.py b/python/vyos/progressbar.py
index 8d1042672..eb8ed474a 100644
--- a/python/vyos/progressbar.py
+++ b/python/vyos/progressbar.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/proto/__init__.py b/python/vyos/proto/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/proto/__init__.py
diff --git a/python/vyos/proto/generate_dataclass.py b/python/vyos/proto/generate_dataclass.py
new file mode 100755
index 000000000..64485cd10
--- /dev/null
+++ b/python/vyos/proto/generate_dataclass.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#
+import argparse
+import os
+
+from google.protobuf.descriptor_pb2 import FileDescriptorSet # pylint: disable=no-name-in-module
+from google.protobuf.descriptor_pb2 import FieldDescriptorProto # pylint: disable=no-name-in-module
+from humps import decamelize
+
+HEADER = """\
+from enum import IntEnum
+from dataclasses import dataclass
+from dataclasses import field
+"""
+
+
+def normalize(s: str) -> str:
+ """Decamelize and avoid syntactic collision"""
+ t = decamelize(s)
+ return t + '_' if t in ['from'] else t
+
+
+def generate_dataclass(descriptor_proto):
+ class_name = descriptor_proto.name
+ fields = []
+ for field_p in descriptor_proto.field:
+ field_name = field_p.name
+ field_type, field_default = get_type(field_p.type, field_p.type_name)
+ match field_p.label:
+ case FieldDescriptorProto.LABEL_REPEATED:
+ field_type = f'list[{field_type}] = field(default_factory=list)'
+ case FieldDescriptorProto.LABEL_OPTIONAL:
+ field_type = f'{field_type} = None'
+ case _:
+ field_type = f'{field_type} = {field_default}'
+
+ fields.append(f' {field_name}: {field_type}')
+
+ code = f"""
+@dataclass
+class {class_name}:
+{chr(10).join(fields) if fields else ' pass'}
+"""
+
+ return code
+
+
+def generate_request(descriptor_proto):
+ class_name = descriptor_proto.name
+ fields = []
+ f_vars = []
+ for field_p in descriptor_proto.field:
+ field_name = field_p.name
+ field_type, field_default = get_type(field_p.type, field_p.type_name)
+ match field_p.label:
+ case FieldDescriptorProto.LABEL_REPEATED:
+ field_type = f'list[{field_type}] = []'
+ case FieldDescriptorProto.LABEL_OPTIONAL:
+ field_type = f'{field_type} = None'
+ case _:
+ field_type = f'{field_type} = {field_default}'
+
+ fields.append(f'{normalize(field_name)}: {field_type}')
+ f_vars.append(f'{normalize(field_name)}')
+
+ fields.insert(0, 'token: str = None')
+
+ code = f"""
+def set_request_{decamelize(class_name)}({', '.join(fields)}):
+ reqi = {class_name} ({', '.join(f_vars)})
+ req = Request({decamelize(class_name)}=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+"""
+
+ return code
+
+
+def generate_nested_dataclass(descriptor_proto):
+ out = ''
+ for nested_p in descriptor_proto.nested_type:
+ out = out + generate_dataclass(nested_p)
+
+ return out
+
+
+def generate_nested_request(descriptor_proto):
+ out = ''
+ for nested_p in descriptor_proto.nested_type:
+ out = out + generate_request(nested_p)
+
+ return out
+
+
+def generate_enum_dataclass(descriptor_proto):
+ code = ''
+ for enum_p in descriptor_proto.enum_type:
+ enums = []
+ enum_name = enum_p.name
+ for enum_val in enum_p.value:
+ enums.append(f' {enum_val.name} = {enum_val.number}')
+
+ code += f"""
+class {enum_name}(IntEnum):
+{chr(10).join(enums)}
+"""
+
+ return code
+
+
+def get_type(field_type, type_name):
+ res = 'Any', None
+ match field_type:
+ case FieldDescriptorProto.TYPE_STRING:
+ res = 'str', '""'
+ case FieldDescriptorProto.TYPE_INT32 | FieldDescriptorProto.TYPE_INT64:
+ res = 'int', 0
+ case FieldDescriptorProto.TYPE_FLOAT | FieldDescriptorProto.TYPE_DOUBLE:
+ res = 'float', 0.0
+ case FieldDescriptorProto.TYPE_BOOL:
+ res = 'bool', False
+ case FieldDescriptorProto.TYPE_MESSAGE | FieldDescriptorProto.TYPE_ENUM:
+ res = type_name.split('.')[-1], None
+ case _:
+ pass
+
+ return res
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('descriptor_file', help='protobuf .desc file')
+ parser.add_argument('--out-dir', help='directory to write generated file')
+ args = parser.parse_args()
+ desc_file = args.descriptor_file
+ out_dir = args.out_dir
+
+ with open(desc_file, 'rb') as f:
+ descriptor_set_data = f.read()
+
+ descriptor_set = FileDescriptorSet()
+ descriptor_set.ParseFromString(descriptor_set_data)
+
+ for file_proto in descriptor_set.file:
+ f = f'{file_proto.name.replace(".", "_")}.py'
+ f = os.path.join(out_dir, f)
+ dataclass_code = ''
+ nested_code = ''
+ enum_code = ''
+ request_code = ''
+ with open(f, 'w') as f:
+ enum_code += generate_enum_dataclass(file_proto)
+ for message_proto in file_proto.message_type:
+ dataclass_code += generate_dataclass(message_proto)
+ nested_code += generate_nested_dataclass(message_proto)
+ enum_code += generate_enum_dataclass(message_proto)
+ request_code += generate_nested_request(message_proto)
+
+ f.write(HEADER)
+ f.write(enum_code)
+ f.write(nested_code)
+ f.write(dataclass_code)
+ f.write(request_code)
diff --git a/python/vyos/proto/vycall_pb2.py b/python/vyos/proto/vycall_pb2.py
new file mode 100644
index 000000000..95214d2a6
--- /dev/null
+++ b/python/vyos/proto/vycall_pb2.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: vycall.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cvycall.proto\"&\n\x06Status\x12\x0f\n\x07success\x18\x01 \x02(\x08\x12\x0b\n\x03out\x18\x02 \x02(\t\"Y\n\x04\x43\x61ll\x12\x13\n\x0bscript_name\x18\x01 \x02(\t\x12\x11\n\ttag_value\x18\x02 \x01(\t\x12\x11\n\targ_value\x18\x03 \x01(\t\x12\x16\n\x05reply\x18\x04 \x01(\x0b\x32\x07.Status\"~\n\x06\x43ommit\x12\x12\n\nsession_id\x18\x01 \x02(\t\x12\x0f\n\x07\x64ry_run\x18\x04 \x02(\x08\x12\x0e\n\x06\x61tomic\x18\x05 \x02(\x08\x12\x12\n\nbackground\x18\x06 \x02(\x08\x12\x15\n\x04init\x18\x07 \x01(\x0b\x32\x07.Status\x12\x14\n\x05\x63\x61lls\x18\x08 \x03(\x0b\x32\x05.Call')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'vycall_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ _STATUS._serialized_start=16
+ _STATUS._serialized_end=54
+ _CALL._serialized_start=56
+ _CALL._serialized_end=145
+ _COMMIT._serialized_start=147
+ _COMMIT._serialized_end=273
+# @@protoc_insertion_point(module_scope)
diff --git a/python/vyos/proto/vyconf_client.py b/python/vyos/proto/vyconf_client.py
new file mode 100644
index 000000000..a3ba9864c
--- /dev/null
+++ b/python/vyos/proto/vyconf_client.py
@@ -0,0 +1,89 @@
+# Copyright 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/>.
+
+import socket
+from dataclasses import asdict
+
+from vyos.proto import vyconf_proto
+from vyos.proto import vyconf_pb2
+
+from google.protobuf.json_format import MessageToDict
+from google.protobuf.json_format import ParseDict
+
+socket_path = '/var/run/vyconfd.sock'
+
+
+def send_socket(msg: bytearray) -> bytes:
+ data = bytes()
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ client.connect(socket_path)
+ client.sendall(msg)
+
+ data_length = client.recv(4)
+ if data_length:
+ length = int.from_bytes(data_length)
+ data = client.recv(length)
+
+ client.close()
+
+ return data
+
+
+def request_to_msg(req: vyconf_proto.RequestEnvelope) -> vyconf_pb2.RequestEnvelope:
+ # pylint: disable=no-member
+
+ msg = vyconf_pb2.RequestEnvelope()
+ msg = ParseDict(asdict(req), msg, ignore_unknown_fields=True)
+ return msg
+
+
+def msg_to_response(msg: vyconf_pb2.Response) -> vyconf_proto.Response:
+ # pylint: disable=no-member
+
+ d = MessageToDict(
+ msg, preserving_proto_field_name=True, use_integers_for_enums=True
+ )
+
+ response = vyconf_proto.Response(**d)
+ return response
+
+
+def write_request(req: vyconf_proto.RequestEnvelope) -> bytearray:
+ req_msg = request_to_msg(req)
+ encoded_data = req_msg.SerializeToString()
+ byte_size = req_msg.ByteSize()
+ length_bytes = byte_size.to_bytes(4)
+ arr = bytearray(length_bytes)
+ arr.extend(encoded_data)
+
+ return arr
+
+
+def read_response(msg: bytes) -> vyconf_proto.Response:
+ response_msg = vyconf_pb2.Response() # pylint: disable=no-member
+ response_msg.ParseFromString(msg)
+ response = msg_to_response(response_msg)
+
+ return response
+
+
+def send_request(name, *args, **kwargs):
+ func = getattr(vyconf_proto, f'set_request_{name}')
+ request_env = func(*args, **kwargs)
+ msg = write_request(request_env)
+ response_msg = send_socket(msg)
+ response = read_response(response_msg)
+
+ return response
diff --git a/python/vyos/proto/vyconf_pb2.py b/python/vyos/proto/vyconf_pb2.py
new file mode 100644
index 000000000..4bf0eb2e0
--- /dev/null
+++ b/python/vyos/proto/vyconf_pb2.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: vyconf.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cvyconf.proto\"\xae\x15\n\x07Request\x12!\n\x06prompt\x18\x01 \x01(\x0b\x32\x0f.Request.PromptH\x00\x12.\n\rsetup_session\x18\x02 \x01(\x0b\x32\x15.Request.SetupSessionH\x00\x12\x1b\n\x03set\x18\x03 \x01(\x0b\x32\x0c.Request.SetH\x00\x12!\n\x06\x64\x65lete\x18\x04 \x01(\x0b\x32\x0f.Request.DeleteH\x00\x12!\n\x06rename\x18\x05 \x01(\x0b\x32\x0f.Request.RenameH\x00\x12\x1d\n\x04\x63opy\x18\x06 \x01(\x0b\x32\r.Request.CopyH\x00\x12#\n\x07\x63omment\x18\x07 \x01(\x0b\x32\x10.Request.CommentH\x00\x12!\n\x06\x63ommit\x18\x08 \x01(\x0b\x32\x0f.Request.CommitH\x00\x12%\n\x08rollback\x18\t \x01(\x0b\x32\x11.Request.RollbackH\x00\x12\x1f\n\x05merge\x18\n \x01(\x0b\x32\x0e.Request.MergeH\x00\x12\x1d\n\x04save\x18\x0b \x01(\x0b\x32\r.Request.SaveH\x00\x12*\n\x0bshow_config\x18\x0c \x01(\x0b\x32\x13.Request.ShowConfigH\x00\x12!\n\x06\x65xists\x18\r \x01(\x0b\x32\x0f.Request.ExistsH\x00\x12&\n\tget_value\x18\x0e \x01(\x0b\x32\x11.Request.GetValueH\x00\x12(\n\nget_values\x18\x0f \x01(\x0b\x32\x12.Request.GetValuesH\x00\x12.\n\rlist_children\x18\x10 \x01(\x0b\x32\x15.Request.ListChildrenH\x00\x12)\n\x0brun_op_mode\x18\x11 \x01(\x0b\x32\x12.Request.RunOpModeH\x00\x12#\n\x07\x63onfirm\x18\x12 \x01(\x0b\x32\x10.Request.ConfirmH\x00\x12\x43\n\x18\x65nter_configuration_mode\x18\x13 \x01(\x0b\x32\x1f.Request.EnterConfigurationModeH\x00\x12\x41\n\x17\x65xit_configuration_mode\x18\x14 \x01(\x0b\x32\x1e.Request.ExitConfigurationModeH\x00\x12%\n\x08validate\x18\x15 \x01(\x0b\x32\x11.Request.ValidateH\x00\x12%\n\x08teardown\x18\x16 \x01(\x0b\x32\x11.Request.TeardownH\x00\x12\x30\n\x0ereload_reftree\x18\x17 \x01(\x0b\x32\x16.Request.ReloadReftreeH\x00\x12\x1d\n\x04load\x18\x18 \x01(\x0b\x32\r.Request.LoadH\x00\x12#\n\x07\x64iscard\x18\x19 \x01(\x0b\x32\x10.Request.DiscardH\x00\x12\x32\n\x0fsession_changed\x18\x1a \x01(\x0b\x32\x17.Request.SessionChangedH\x00\x12/\n\x0esession_of_pid\x18\x1b \x01(\x0b\x32\x15.Request.SessionOfPidH\x00\x12\x37\n\x12session_update_pid\x18\x1c \x01(\x0b\x32\x19.Request.SessionUpdatePidH\x00\x12(\n\nget_config\x18\x1d \x01(\x0b\x32\x12.Request.GetConfigH\x00\x1a\x08\n\x06Prompt\x1aP\n\x0cSetupSession\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x12\x19\n\x11\x43lientApplication\x18\x02 \x01(\t\x12\x12\n\nOnBehalfOf\x18\x03 \x01(\x05\x1a!\n\x0cSessionOfPid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a%\n\x10SessionUpdatePid\x12\x11\n\tClientPid\x18\x01 \x02(\x05\x1a\x1a\n\tGetConfig\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1e\n\x08Teardown\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\x1a\x46\n\x08Validate\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\x13\n\x03Set\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x16\n\x06\x44\x65lete\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x18\n\x07\x44iscard\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x1f\n\x0eSessionChanged\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\x1a\x35\n\x06Rename\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a\x33\n\x04\x43opy\x12\x11\n\tEditLevel\x18\x01 \x03(\t\x12\x0c\n\x04\x46rom\x18\x02 \x02(\t\x12\n\n\x02To\x18\x03 \x02(\t\x1a(\n\x07\x43omment\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12\x0f\n\x07\x43omment\x18\x02 \x02(\t\x1aR\n\x06\x43ommit\x12\x0f\n\x07\x43onfirm\x18\x01 \x01(\x08\x12\x16\n\x0e\x43onfirmTimeout\x18\x02 \x01(\x05\x12\x0f\n\x07\x43omment\x18\x03 \x01(\t\x12\x0e\n\x06\x44ryRun\x18\x04 \x01(\x08\x1a\x1c\n\x08Rollback\x12\x10\n\x08Revision\x18\x01 \x02(\x05\x1aO\n\x04Load\x12\x10\n\x08Location\x18\x01 \x02(\t\x12\x0e\n\x06\x63\x61\x63hed\x18\x02 \x02(\x08\x12%\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x15.Request.ConfigFormat\x1aU\n\x05Merge\x12\x10\n\x08Location\x18\x01 \x02(\t\x12\x13\n\x0b\x64\x65structive\x18\x02 \x02(\x08\x12%\n\x06\x66ormat\x18\x03 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a?\n\x04Save\x12\x10\n\x08Location\x18\x01 \x02(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x41\n\nShowConfig\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12%\n\x06\x66ormat\x18\x02 \x01(\x0e\x32\x15.Request.ConfigFormat\x1a\x16\n\x06\x45xists\x12\x0c\n\x04Path\x18\x01 \x03(\t\x1a\x46\n\x08GetValue\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tGetValues\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aJ\n\x0cListChildren\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1aG\n\tRunOpMode\x12\x0c\n\x04Path\x18\x01 \x03(\t\x12,\n\routput_format\x18\x02 \x01(\x0e\x32\x15.Request.OutputFormat\x1a\t\n\x07\x43onfirm\x1a\x46\n\x16\x45nterConfigurationMode\x12\x11\n\tExclusive\x18\x01 \x02(\x08\x12\x19\n\x11OverrideExclusive\x18\x02 \x02(\x08\x1a\x17\n\x15\x45xitConfigurationMode\x1a#\n\rReloadReftree\x12\x12\n\nOnBehalfOf\x18\x01 \x01(\x05\"#\n\x0c\x43onfigFormat\x12\t\n\x05\x43URLY\x10\x00\x12\x08\n\x04JSON\x10\x01\")\n\x0cOutputFormat\x12\x0c\n\x08OutPlain\x10\x00\x12\x0b\n\x07OutJSON\x10\x01\x42\x05\n\x03msg\";\n\x0fRequestEnvelope\x12\r\n\x05token\x18\x01 \x01(\t\x12\x19\n\x07request\x18\x02 \x02(\x0b\x32\x08.Request\"S\n\x08Response\x12\x17\n\x06status\x18\x01 \x02(\x0e\x32\x07.Errnum\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\r\n\x05\x65rror\x18\x03 \x01(\t\x12\x0f\n\x07warning\x18\x04 \x01(\t*\xd2\x01\n\x06\x45rrnum\x12\x0b\n\x07SUCCESS\x10\x00\x12\x08\n\x04\x46\x41IL\x10\x01\x12\x10\n\x0cINVALID_PATH\x10\x02\x12\x11\n\rINVALID_VALUE\x10\x03\x12\x16\n\x12\x43OMMIT_IN_PROGRESS\x10\x04\x12\x18\n\x14\x43ONFIGURATION_LOCKED\x10\x05\x12\x12\n\x0eINTERNAL_ERROR\x10\x06\x12\x15\n\x11PERMISSION_DENIED\x10\x07\x12\x17\n\x13PATH_ALREADY_EXISTS\x10\x08\x12\x16\n\x12UNCOMMITED_CHANGES\x10\t')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'vyconf_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+ DESCRIPTOR._options = None
+ _ERRNUM._serialized_start=2900
+ _ERRNUM._serialized_end=3110
+ _REQUEST._serialized_start=17
+ _REQUEST._serialized_end=2751
+ _REQUEST_PROMPT._serialized_start=1237
+ _REQUEST_PROMPT._serialized_end=1245
+ _REQUEST_SETUPSESSION._serialized_start=1247
+ _REQUEST_SETUPSESSION._serialized_end=1327
+ _REQUEST_SESSIONOFPID._serialized_start=1329
+ _REQUEST_SESSIONOFPID._serialized_end=1362
+ _REQUEST_SESSIONUPDATEPID._serialized_start=1364
+ _REQUEST_SESSIONUPDATEPID._serialized_end=1401
+ _REQUEST_GETCONFIG._serialized_start=1403
+ _REQUEST_GETCONFIG._serialized_end=1429
+ _REQUEST_TEARDOWN._serialized_start=1431
+ _REQUEST_TEARDOWN._serialized_end=1461
+ _REQUEST_VALIDATE._serialized_start=1463
+ _REQUEST_VALIDATE._serialized_end=1533
+ _REQUEST_SET._serialized_start=1535
+ _REQUEST_SET._serialized_end=1554
+ _REQUEST_DELETE._serialized_start=1556
+ _REQUEST_DELETE._serialized_end=1578
+ _REQUEST_DISCARD._serialized_start=1580
+ _REQUEST_DISCARD._serialized_end=1604
+ _REQUEST_SESSIONCHANGED._serialized_start=1606
+ _REQUEST_SESSIONCHANGED._serialized_end=1637
+ _REQUEST_RENAME._serialized_start=1639
+ _REQUEST_RENAME._serialized_end=1692
+ _REQUEST_COPY._serialized_start=1694
+ _REQUEST_COPY._serialized_end=1745
+ _REQUEST_COMMENT._serialized_start=1747
+ _REQUEST_COMMENT._serialized_end=1787
+ _REQUEST_COMMIT._serialized_start=1789
+ _REQUEST_COMMIT._serialized_end=1871
+ _REQUEST_ROLLBACK._serialized_start=1873
+ _REQUEST_ROLLBACK._serialized_end=1901
+ _REQUEST_LOAD._serialized_start=1903
+ _REQUEST_LOAD._serialized_end=1982
+ _REQUEST_MERGE._serialized_start=1984
+ _REQUEST_MERGE._serialized_end=2069
+ _REQUEST_SAVE._serialized_start=2071
+ _REQUEST_SAVE._serialized_end=2134
+ _REQUEST_SHOWCONFIG._serialized_start=2136
+ _REQUEST_SHOWCONFIG._serialized_end=2201
+ _REQUEST_EXISTS._serialized_start=2203
+ _REQUEST_EXISTS._serialized_end=2225
+ _REQUEST_GETVALUE._serialized_start=2227
+ _REQUEST_GETVALUE._serialized_end=2297
+ _REQUEST_GETVALUES._serialized_start=2299
+ _REQUEST_GETVALUES._serialized_end=2370
+ _REQUEST_LISTCHILDREN._serialized_start=2372
+ _REQUEST_LISTCHILDREN._serialized_end=2446
+ _REQUEST_RUNOPMODE._serialized_start=2448
+ _REQUEST_RUNOPMODE._serialized_end=2519
+ _REQUEST_CONFIRM._serialized_start=1799
+ _REQUEST_CONFIRM._serialized_end=1808
+ _REQUEST_ENTERCONFIGURATIONMODE._serialized_start=2532
+ _REQUEST_ENTERCONFIGURATIONMODE._serialized_end=2602
+ _REQUEST_EXITCONFIGURATIONMODE._serialized_start=2604
+ _REQUEST_EXITCONFIGURATIONMODE._serialized_end=2627
+ _REQUEST_RELOADREFTREE._serialized_start=2629
+ _REQUEST_RELOADREFTREE._serialized_end=2664
+ _REQUEST_CONFIGFORMAT._serialized_start=2666
+ _REQUEST_CONFIGFORMAT._serialized_end=2701
+ _REQUEST_OUTPUTFORMAT._serialized_start=2703
+ _REQUEST_OUTPUTFORMAT._serialized_end=2744
+ _REQUESTENVELOPE._serialized_start=2753
+ _REQUESTENVELOPE._serialized_end=2812
+ _RESPONSE._serialized_start=2814
+ _RESPONSE._serialized_end=2897
+# @@protoc_insertion_point(module_scope)
diff --git a/python/vyos/proto/vyconf_proto.py b/python/vyos/proto/vyconf_proto.py
new file mode 100644
index 000000000..ec62a6e35
--- /dev/null
+++ b/python/vyos/proto/vyconf_proto.py
@@ -0,0 +1,379 @@
+from enum import IntEnum
+from dataclasses import dataclass
+from dataclasses import field
+
+class Errnum(IntEnum):
+ SUCCESS = 0
+ FAIL = 1
+ INVALID_PATH = 2
+ INVALID_VALUE = 3
+ COMMIT_IN_PROGRESS = 4
+ CONFIGURATION_LOCKED = 5
+ INTERNAL_ERROR = 6
+ PERMISSION_DENIED = 7
+ PATH_ALREADY_EXISTS = 8
+ UNCOMMITED_CHANGES = 9
+
+class ConfigFormat(IntEnum):
+ CURLY = 0
+ JSON = 1
+
+class OutputFormat(IntEnum):
+ OutPlain = 0
+ OutJSON = 1
+
+@dataclass
+class Prompt:
+ pass
+
+@dataclass
+class SetupSession:
+ ClientPid: int = 0
+ ClientApplication: str = None
+ OnBehalfOf: int = None
+
+@dataclass
+class SessionOfPid:
+ ClientPid: int = 0
+
+@dataclass
+class SessionUpdatePid:
+ ClientPid: int = 0
+
+@dataclass
+class GetConfig:
+ dummy: int = None
+
+@dataclass
+class Teardown:
+ OnBehalfOf: int = None
+
+@dataclass
+class Validate:
+ Path: list[str] = field(default_factory=list)
+ output_format: OutputFormat = None
+
+@dataclass
+class Set:
+ Path: list[str] = field(default_factory=list)
+
+@dataclass
+class Delete:
+ Path: list[str] = field(default_factory=list)
+
+@dataclass
+class Discard:
+ dummy: int = None
+
+@dataclass
+class SessionChanged:
+ dummy: int = None
+
+@dataclass
+class Rename:
+ EditLevel: list[str] = field(default_factory=list)
+ From: str = ""
+ To: str = ""
+
+@dataclass
+class Copy:
+ EditLevel: list[str] = field(default_factory=list)
+ From: str = ""
+ To: str = ""
+
+@dataclass
+class Comment:
+ Path: list[str] = field(default_factory=list)
+ Comment: str = ""
+
+@dataclass
+class Commit:
+ Confirm: bool = None
+ ConfirmTimeout: int = None
+ Comment: str = None
+ DryRun: bool = None
+
+@dataclass
+class Rollback:
+ Revision: int = 0
+
+@dataclass
+class Load:
+ Location: str = ""
+ cached: bool = False
+ format: ConfigFormat = None
+
+@dataclass
+class Merge:
+ Location: str = ""
+ destructive: bool = False
+ format: ConfigFormat = None
+
+@dataclass
+class Save:
+ Location: str = ""
+ format: ConfigFormat = None
+
+@dataclass
+class ShowConfig:
+ Path: list[str] = field(default_factory=list)
+ format: ConfigFormat = None
+
+@dataclass
+class Exists:
+ Path: list[str] = field(default_factory=list)
+
+@dataclass
+class GetValue:
+ Path: list[str] = field(default_factory=list)
+ output_format: OutputFormat = None
+
+@dataclass
+class GetValues:
+ Path: list[str] = field(default_factory=list)
+ output_format: OutputFormat = None
+
+@dataclass
+class ListChildren:
+ Path: list[str] = field(default_factory=list)
+ output_format: OutputFormat = None
+
+@dataclass
+class RunOpMode:
+ Path: list[str] = field(default_factory=list)
+ output_format: OutputFormat = None
+
+@dataclass
+class Confirm:
+ pass
+
+@dataclass
+class EnterConfigurationMode:
+ Exclusive: bool = False
+ OverrideExclusive: bool = False
+
+@dataclass
+class ExitConfigurationMode:
+ pass
+
+@dataclass
+class ReloadReftree:
+ OnBehalfOf: int = None
+
+@dataclass
+class Request:
+ prompt: Prompt = None
+ setup_session: SetupSession = None
+ set: Set = None
+ delete: Delete = None
+ rename: Rename = None
+ copy: Copy = None
+ comment: Comment = None
+ commit: Commit = None
+ rollback: Rollback = None
+ merge: Merge = None
+ save: Save = None
+ show_config: ShowConfig = None
+ exists: Exists = None
+ get_value: GetValue = None
+ get_values: GetValues = None
+ list_children: ListChildren = None
+ run_op_mode: RunOpMode = None
+ confirm: Confirm = None
+ enter_configuration_mode: EnterConfigurationMode = None
+ exit_configuration_mode: ExitConfigurationMode = None
+ validate: Validate = None
+ teardown: Teardown = None
+ reload_reftree: ReloadReftree = None
+ load: Load = None
+ discard: Discard = None
+ session_changed: SessionChanged = None
+ session_of_pid: SessionOfPid = None
+ session_update_pid: SessionUpdatePid = None
+ get_config: GetConfig = None
+
+@dataclass
+class RequestEnvelope:
+ token: str = None
+ request: Request = None
+
+@dataclass
+class Response:
+ status: Errnum = None
+ output: str = None
+ error: str = None
+ warning: str = None
+
+def set_request_prompt(token: str = None):
+ reqi = Prompt ()
+ req = Request(prompt=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_setup_session(token: str = None, client_pid: int = 0, client_application: str = None, on_behalf_of: int = None):
+ reqi = SetupSession (client_pid, client_application, on_behalf_of)
+ req = Request(setup_session=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_session_of_pid(token: str = None, client_pid: int = 0):
+ reqi = SessionOfPid (client_pid)
+ req = Request(session_of_pid=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_session_update_pid(token: str = None, client_pid: int = 0):
+ reqi = SessionUpdatePid (client_pid)
+ req = Request(session_update_pid=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_get_config(token: str = None, dummy: int = None):
+ reqi = GetConfig (dummy)
+ req = Request(get_config=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_teardown(token: str = None, on_behalf_of: int = None):
+ reqi = Teardown (on_behalf_of)
+ req = Request(teardown=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_validate(token: str = None, path: list[str] = [], output_format: OutputFormat = None):
+ reqi = Validate (path, output_format)
+ req = Request(validate=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_set(token: str = None, path: list[str] = []):
+ reqi = Set (path)
+ req = Request(set=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_delete(token: str = None, path: list[str] = []):
+ reqi = Delete (path)
+ req = Request(delete=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_discard(token: str = None, dummy: int = None):
+ reqi = Discard (dummy)
+ req = Request(discard=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_session_changed(token: str = None, dummy: int = None):
+ reqi = SessionChanged (dummy)
+ req = Request(session_changed=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_rename(token: str = None, edit_level: list[str] = [], from_: str = "", to: str = ""):
+ reqi = Rename (edit_level, from_, to)
+ req = Request(rename=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_copy(token: str = None, edit_level: list[str] = [], from_: str = "", to: str = ""):
+ reqi = Copy (edit_level, from_, to)
+ req = Request(copy=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_comment(token: str = None, path: list[str] = [], comment: str = ""):
+ reqi = Comment (path, comment)
+ req = Request(comment=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_commit(token: str = None, confirm: bool = None, confirm_timeout: int = None, comment: str = None, dry_run: bool = None):
+ reqi = Commit (confirm, confirm_timeout, comment, dry_run)
+ req = Request(commit=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_rollback(token: str = None, revision: int = 0):
+ reqi = Rollback (revision)
+ req = Request(rollback=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_load(token: str = None, location: str = "", cached: bool = False, format: ConfigFormat = None):
+ reqi = Load (location, cached, format)
+ req = Request(load=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_merge(token: str = None, location: str = "", destructive: bool = False, format: ConfigFormat = None):
+ reqi = Merge (location, destructive, format)
+ req = Request(merge=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_save(token: str = None, location: str = "", format: ConfigFormat = None):
+ reqi = Save (location, format)
+ req = Request(save=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_show_config(token: str = None, path: list[str] = [], format: ConfigFormat = None):
+ reqi = ShowConfig (path, format)
+ req = Request(show_config=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_exists(token: str = None, path: list[str] = []):
+ reqi = Exists (path)
+ req = Request(exists=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_get_value(token: str = None, path: list[str] = [], output_format: OutputFormat = None):
+ reqi = GetValue (path, output_format)
+ req = Request(get_value=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_get_values(token: str = None, path: list[str] = [], output_format: OutputFormat = None):
+ reqi = GetValues (path, output_format)
+ req = Request(get_values=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_list_children(token: str = None, path: list[str] = [], output_format: OutputFormat = None):
+ reqi = ListChildren (path, output_format)
+ req = Request(list_children=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_run_op_mode(token: str = None, path: list[str] = [], output_format: OutputFormat = None):
+ reqi = RunOpMode (path, output_format)
+ req = Request(run_op_mode=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_confirm(token: str = None):
+ reqi = Confirm ()
+ req = Request(confirm=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_enter_configuration_mode(token: str = None, exclusive: bool = False, override_exclusive: bool = False):
+ reqi = EnterConfigurationMode (exclusive, override_exclusive)
+ req = Request(enter_configuration_mode=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_exit_configuration_mode(token: str = None):
+ reqi = ExitConfigurationMode ()
+ req = Request(exit_configuration_mode=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+
+def set_request_reload_reftree(token: str = None, on_behalf_of: int = None):
+ reqi = ReloadReftree (on_behalf_of)
+ req = Request(reload_reftree=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
diff --git a/python/vyos/qos/__init__.py b/python/vyos/qos/__init__.py
index a2980ccde..4bffda2d2 100644
--- a/python/vyos/qos/__init__.py
+++ b/python/vyos/qos/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py
index b477b5b5e..487249714 100644
--- a/python/vyos/qos/base.py
+++ b/python/vyos/qos/base.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/cake.py b/python/vyos/qos/cake.py
index ca5a26917..05a737649 100644
--- a/python/vyos/qos/cake.py
+++ b/python/vyos/qos/cake.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -54,7 +54,16 @@ class CAKE(QoSBase):
f'Invalid flow isolation parameter: {config["flow_isolation"]}'
)
+ if 'ack_filter' in config:
+ if 'aggressive' in config['ack_filter']:
+ tmp += ' ack-filter-aggressive'
+ else:
+ tmp += ' ack-filter'
+ else:
+ tmp += ' no-ack-filter'
+
tmp += ' nat' if 'flow_isolation_nat' in config else ' nonat'
+ tmp += ' no-split-gso' if 'no_split_gso' in config else ' split-gso'
self._cmd(tmp)
diff --git a/python/vyos/qos/droptail.py b/python/vyos/qos/droptail.py
index 427d43d19..223ab1e64 100644
--- a/python/vyos/qos/droptail.py
+++ b/python/vyos/qos/droptail.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/fairqueue.py b/python/vyos/qos/fairqueue.py
index f41d098fb..8f4fe2d47 100644
--- a/python/vyos/qos/fairqueue.py
+++ b/python/vyos/qos/fairqueue.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/fqcodel.py b/python/vyos/qos/fqcodel.py
index cd2340aa2..d574226ef 100644
--- a/python/vyos/qos/fqcodel.py
+++ b/python/vyos/qos/fqcodel.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/limiter.py b/python/vyos/qos/limiter.py
index 3f5c11112..dce376d3e 100644
--- a/python/vyos/qos/limiter.py
+++ b/python/vyos/qos/limiter.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/netem.py b/python/vyos/qos/netem.py
index 8bdef300b..8fdd75387 100644
--- a/python/vyos/qos/netem.py
+++ b/python/vyos/qos/netem.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/priority.py b/python/vyos/qos/priority.py
index 66d27a639..5f373f696 100644
--- a/python/vyos/qos/priority.py
+++ b/python/vyos/qos/priority.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/randomdetect.py b/python/vyos/qos/randomdetect.py
index a3a39da36..63445bb62 100644
--- a/python/vyos/qos/randomdetect.py
+++ b/python/vyos/qos/randomdetect.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/ratelimiter.py b/python/vyos/qos/ratelimiter.py
index a4f80a1be..b0d7b3072 100644
--- a/python/vyos/qos/ratelimiter.py
+++ b/python/vyos/qos/ratelimiter.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/roundrobin.py b/python/vyos/qos/roundrobin.py
index 509c4069f..d07dc0f52 100644
--- a/python/vyos/qos/roundrobin.py
+++ b/python/vyos/qos/roundrobin.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py
index 9f92ccd8b..3840e7d0e 100644
--- a/python/vyos/qos/trafficshaper.py
+++ b/python/vyos/qos/trafficshaper.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/raid.py b/python/vyos/raid.py
index 7fb794817..4ae63a100 100644
--- a/python/vyos/raid.py
+++ b/python/vyos/raid.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index c54fb6031..b73f486c0 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -1,4 +1,4 @@
-# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -22,6 +22,7 @@ import stat
import sys
import tempfile
import urllib.parse
+import gzip
from contextlib import contextmanager
from pathlib import Path
@@ -44,6 +45,7 @@ from vyos.utils.misc import begin
from vyos.utils.process import cmd, rc_cmd
from vyos.version import get_version
from vyos.base import Warning
+from vyos.defaults import directories
CHUNK_SIZE = 8192
@@ -478,3 +480,45 @@ def get_remote_config(urlstring, source_host='', source_port=0):
return f.read()
finally:
os.remove(temp)
+
+
+def get_config_file(file_in: str, file_out: str, source_host='', source_port=0):
+ protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp']
+ config_dir = directories['config']
+
+ with tempfile.NamedTemporaryFile() as tmp_file:
+ if any(file_in.startswith(f'{x}://') for x in protocols):
+ try:
+ download(
+ tmp_file.name,
+ file_in,
+ check_space=True,
+ source_host='',
+ source_port=0,
+ raise_error=True,
+ )
+ except Exception as e:
+ return e
+ file_name = tmp_file.name
+ else:
+ full_path = os.path.realpath(file_in)
+ if os.path.isfile(full_path):
+ file_in = full_path
+ else:
+ file_in = os.path.join(config_dir, file_in)
+ if not os.path.isfile(file_in):
+ return ValueError(f'No such file {file_in}')
+
+ file_name = file_in
+
+ if file_in.endswith('.gz'):
+ try:
+ with gzip.open(file_name, 'rb') as f_in:
+ with open(file_out, 'wb') as f_out:
+ shutil.copyfileobj(f_in, f_out)
+ except Exception as e:
+ return e
+ else:
+ shutil.copyfile(file_name, file_out)
+
+ return None
diff --git a/python/vyos/snmpv3_hashgen.py b/python/vyos/snmpv3_hashgen.py
index 324c3274d..57dba07a0 100644
--- a/python/vyos/snmpv3_hashgen.py
+++ b/python/vyos/snmpv3_hashgen.py
@@ -1,4 +1,4 @@
-# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/system/__init__.py b/python/vyos/system/__init__.py
index 0c91330ba..42af8e3e8 100644
--- a/python/vyos/system/__init__.py
+++ b/python/vyos/system/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py
index d35bddea2..23a34d38a 100644
--- a/python/vyos/system/compat.py
+++ b/python/vyos/system/compat.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/system/disk.py b/python/vyos/system/disk.py
index c8908cd5c..268a3b195 100644
--- a/python/vyos/system/disk.py
+++ b/python/vyos/system/disk.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py
index de8303ee2..0f04fa5e9 100644
--- a/python/vyos/system/grub.py
+++ b/python/vyos/system/grub.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/system/grub_util.py b/python/vyos/system/grub_util.py
index 4a3d8795e..e534334e6 100644
--- a/python/vyos/system/grub_util.py
+++ b/python/vyos/system/grub_util.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -56,13 +56,12 @@ def set_kernel_cmdline_options(cmdline_options: str, version: str = '',
@image.if_not_live_boot
def update_kernel_cmdline_options(cmdline_options: str,
- root_dir: str = '') -> None:
+ root_dir: str = '',
+ version = image.get_running_image()) -> None:
"""Update Kernel custom cmdline options"""
if not root_dir:
root_dir = disk.find_persistence()
- version = image.get_running_image()
-
boot_opts_current = grub.get_boot_opts(version, root_dir)
boot_opts_proposed = grub.BOOT_OPTS_STEM + f'{version} {cmdline_options}'
diff --git a/python/vyos/system/image.py b/python/vyos/system/image.py
index aae52e770..ed8a96fbb 100644
--- a/python/vyos/system/image.py
+++ b/python/vyos/system/image.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/system/raid.py b/python/vyos/system/raid.py
index 5b33d34da..c03764ad1 100644
--- a/python/vyos/system/raid.py
+++ b/python/vyos/system/raid.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/template.py b/python/vyos/template.py
index be9f781a6..824d42136 100755
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -36,6 +36,7 @@ DEFAULT_TEMPLATE_DIR = directories["templates"]
# Holds template filters registered via register_filter()
_FILTERS = {}
_TESTS = {}
+_CLEVER_FUNCTIONS = {}
# reuse Environments with identical settings to improve performance
@functools.lru_cache(maxsize=2)
@@ -58,6 +59,7 @@ def _get_environment(location=None):
)
env.filters.update(_FILTERS)
env.tests.update(_TESTS)
+ env.globals.update(_CLEVER_FUNCTIONS)
return env
@@ -77,7 +79,7 @@ def register_filter(name, func=None):
"Filters can only be registered before rendering the first template"
)
if name in _FILTERS:
- raise ValueError(f"A filter with name {name!r} was registered already")
+ raise ValueError(f"A filter with name {name!r} was already registered")
_FILTERS[name] = func
return func
@@ -97,10 +99,30 @@ def register_test(name, func=None):
"Tests can only be registered before rendering the first template"
)
if name in _TESTS:
- raise ValueError(f"A test with name {name!r} was registered already")
+ raise ValueError(f"A test with name {name!r} was already registered")
_TESTS[name] = func
return func
+def register_clever_function(name, func=None):
+ """Register a function to be available as test in templates under given name.
+
+ It can also be used as a decorator, see below in this module for examples.
+
+ :raise RuntimeError:
+ when trying to register a test after a template has been rendered already
+ :raise ValueError: when trying to register a name which was taken already
+ """
+ if func is None:
+ return functools.partial(register_clever_function, name)
+ if _get_environment.cache_info().currsize:
+ raise RuntimeError(
+ "Clever functions can only be registered before rendering the" \
+ "first template")
+ if name in _CLEVER_FUNCTIONS:
+ raise ValueError(f"A clever function with name {name!r} was already "\
+ "registered")
+ _CLEVER_FUNCTIONS[name] = func
+ return func
def render_to_string(template, content, formater=None, location=None):
"""Render a template from the template directory, raise on any errors.
@@ -150,6 +172,8 @@ def render(
# As we are opening the file with 'w', we are performing the rendering before
# calling open() to not accidentally erase the file if rendering fails
rendered = render_to_string(template, content, formater, location)
+ # Remove any trailing character and always add a new line at the end
+ rendered = rendered.rstrip() + "\n"
# Write to file
with open(destination, "w") as file:
@@ -390,28 +414,6 @@ def compare_netmask(netmask1, netmask2):
except:
return False
-@register_filter('isc_static_route')
-def isc_static_route(subnet, router):
- # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
- # Option format is:
- # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
- # where bytes with the value 0 are omitted.
- from ipaddress import ip_network
- net = ip_network(subnet)
- # add netmask
- string = str(net.prefixlen) + ','
- # add network bytes
- if net.prefixlen:
- width = net.prefixlen // 8
- if net.prefixlen % 8:
- width += 1
- string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
-
- # add router bytes
- string += ','.join(router.split('.'))
-
- return string
-
@register_filter('is_file')
def is_file(filename):
if os.path.exists(filename):
@@ -580,6 +582,10 @@ def snmp_auth_oid(type):
}
return OIDs[type]
+@register_filter('quoted_join')
+def quoted_join(input_list, join_str, quote='"'):
+ return str(join_str).join(f'{quote}{elem}{quote}' for elem in input_list)
+
@register_filter('nft_action')
def nft_action(vyos_action):
if vyos_action == 'accept':
@@ -612,12 +618,17 @@ def nft_default_rule(fw_conf, fw_name, family):
return " ".join(output)
@register_filter('nft_state_policy')
-def nft_state_policy(conf, state):
+def nft_state_policy(conf, state, bridge=False):
out = [f'ct state {state}']
+ action = conf['action'] if 'action' in conf else None
+
+ if bridge and action == 'reject':
+ action = 'drop' # T7148 - Bridge cannot use reject
+
if 'log' in conf:
log_state = state[:3].upper()
- log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper()
+ log_action = (action if action else 'accept')[:1].upper()
out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"')
if 'log_level' in conf:
@@ -626,8 +637,8 @@ def nft_state_policy(conf, state):
out.append('counter')
- if 'action' in conf:
- out.append(conf['action'])
+ if action:
+ out.append(action)
return " ".join(out)
@@ -667,6 +678,29 @@ def nft_nested_group(out_list, includes, groups, key):
add_includes(name)
return out_list
+@register_filter('nft_accept_invalid')
+def nft_accept_invalid(ether_type):
+ ether_type_mapping = {
+ 'dhcp': 'udp sport 67 udp dport 68',
+ 'arp': 'arp',
+ 'pppoe-discovery': '0x8863',
+ 'pppoe': '0x8864',
+ '802.1q': '8021q',
+ '802.1ad': '8021ad',
+ 'wol': '0x0842',
+ }
+ if ether_type not in ether_type_mapping:
+ raise RuntimeError(f'Ethernet type "{ether_type}" not found in ' \
+ 'available ethernet types!')
+ out = 'ct state invalid '
+
+ if ether_type != 'dhcp':
+ out += 'ether type '
+
+ out += f'{ether_type_mapping[ether_type]} counter accept'
+
+ return out
+
@register_filter('nat_rule')
def nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
from vyos.nat import parse_nat_rule
@@ -721,7 +755,7 @@ def conntrack_rule(rule_conf, rule_id, action, ipv6=False):
if port[0] == '!':
operator = '!='
port = port[1:]
- output.append(f'th {prefix}port {operator} {port}')
+ output.append(f'th {prefix}port {operator} {{ {port} }}')
if 'group' in side_conf:
group = side_conf['group']
@@ -779,6 +813,11 @@ def conntrack_ct_policy(protocol_conf):
return ", ".join(output)
+@register_filter('wlb_nft_rule')
+def wlb_nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False):
+ from vyos.wanloadbalance import nft_rule as wlb_nft_rule
+ return wlb_nft_rule(rule_conf, rule_id, local, exclude, limit, weight, health_state, action, restore_mark)
+
@register_filter('range_to_regex')
def range_to_regex(num_range):
"""Convert range of numbers or list of ranges
@@ -871,10 +910,77 @@ def kea_high_availability_json(config):
return dumps(data)
+@register_filter('kea_dynamic_dns_update_main_json')
+def kea_dynamic_dns_update_main_json(config):
+ from vyos.kea import kea_parse_ddns_settings
+ from json import dumps
+
+ data = kea_parse_ddns_settings(config)
+
+ if len(data) == 0:
+ return ''
+
+ return dumps(data, indent=8)[1:-1] + ','
+
+@register_filter('kea_dynamic_dns_update_tsig_key_json')
+def kea_dynamic_dns_update_tsig_key_json(config):
+ from vyos.kea import kea_parse_tsig_algo
+ from json import dumps
+ out = []
+
+ if 'tsig_key' not in config:
+ return dumps(out)
+
+ tsig_keys = config['tsig_key']
+
+ for tsig_key_name, tsig_key_config in tsig_keys.items():
+ tsig_key = {
+ 'name': tsig_key_name,
+ 'algorithm': kea_parse_tsig_algo(tsig_key_config['algorithm']),
+ 'secret': tsig_key_config['secret']
+ }
+ out.append(tsig_key)
+
+ return dumps(out, indent=12)
+
+@register_filter('kea_dynamic_dns_update_domains')
+def kea_dynamic_dns_update_domains(config, type_key):
+ from json import dumps
+ out = []
+
+ if type_key not in config:
+ return dumps(out)
+
+ domains = config[type_key]
+
+ for domain_name, domain_config in domains.items():
+ domain = {
+ 'name': domain_name,
+
+ }
+ if 'key_name' in domain_config:
+ domain['key-name'] = domain_config['key_name']
+
+ if 'dns_server' in domain_config:
+ dns_servers = []
+ for dns_server_config in domain_config['dns_server'].values():
+ dns_server = {
+ 'ip-address': dns_server_config['address']
+ }
+ if 'port' in dns_server_config:
+ dns_server['port'] = int(dns_server_config['port'])
+ dns_servers.append(dns_server)
+ domain['dns-servers'] = dns_servers
+
+ out.append(domain)
+
+ return dumps(out, indent=12)
+
@register_filter('kea_shared_network_json')
def kea_shared_network_json(shared_networks):
from vyos.kea import kea_parse_options
from vyos.kea import kea_parse_subnet
+ from vyos.kea import kea_parse_ddns_settings
from json import dumps
out = []
@@ -885,9 +991,13 @@ def kea_shared_network_json(shared_networks):
network = {
'name': name,
'authoritative': ('authoritative' in config),
- 'subnet4': []
+ 'subnet4': [],
+ 'user-context': {}
}
+ if 'dynamic_dns_update' in config:
+ network.update(kea_parse_ddns_settings(config['dynamic_dns_update']))
+
if 'option' in config:
network['option-data'] = kea_parse_options(config['option'])
@@ -897,6 +1007,9 @@ def kea_shared_network_json(shared_networks):
if 'bootfile_server' in config['option']:
network['next-server'] = config['option']['bootfile_server']
+ if 'ping_check' in config:
+ network['user-context']['enable-ping-check'] = True
+
if 'subnet' in config:
for subnet, subnet_config in config['subnet'].items():
if 'disable' in subnet_config:
@@ -988,3 +1101,39 @@ def vyos_defined(value, test_value=None, var_type=None):
else:
# Valid value and is matching optional argument if provided - return true
return True
+
+@register_clever_function('get_default_port')
+def get_default_port(service):
+ """
+ Jinja2 plugin to retrieve common service port number from vyos.defaults
+ class from a Jinja2 template. This removes the need to hardcode, or pass in
+ the data using the general dictionary.
+
+ Added to remove code complexity and make it easier to read.
+
+ Example:
+ {{ get_default_port('certbot_haproxy') }}
+ """
+ from vyos.defaults import internal_ports
+ if service not in internal_ports:
+ raise RuntimeError(f'Service "{service}" not found in internal ' \
+ 'vyos.defaults.internal_ports dict!')
+ return internal_ports[service]
+
+@register_clever_function('get_default_config_file')
+def get_default_config_file(filename):
+ """
+ Jinja2 plugin to retrieve a common configuration file path from
+ vyos.defaults class from a Jinja2 template. This removes the need to
+ hardcode, or pass in the data using the general dictionary.
+
+ Added to remove code complexity and make it easier to read.
+
+ Example:
+ {{ get_default_config_file('certbot_haproxy') }}
+ """
+ from vyos.defaults import config_files
+ if filename not in config_files:
+ raise RuntimeError(f'Configuration file "{filename}" not found in '\
+ 'internal vyos.defaults.config_files dict!')
+ return config_files[filename]
diff --git a/python/vyos/tpm.py b/python/vyos/tpm.py
index a24f149fd..663490dec 100644
--- a/python/vyos/tpm.py
+++ b/python/vyos/tpm.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
diff --git a/python/vyos/utils/__init__.py b/python/vyos/utils/__init__.py
index 3759b2125..280cde17f 100644
--- a/python/vyos/utils/__init__.py
+++ b/python/vyos/utils/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/assertion.py b/python/vyos/utils/assertion.py
index c7fa220c3..aa0614743 100644
--- a/python/vyos/utils/assertion.py
+++ b/python/vyos/utils/assertion.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py
index a0b3e1cae..6e816af71 100644
--- a/python/vyos/utils/auth.py
+++ b/python/vyos/utils/auth.py
@@ -1,6 +1,6 @@
# authutils -- miscelanneous functions for handling passwords and publis keys
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright 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;
@@ -13,10 +13,80 @@
# You should have received a copy of the GNU Lesser General Public License along with this library;
# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+import cracklib
+import math
import re
+import string
+from enum import StrEnum
+from decimal import Decimal
from vyos.utils.process import cmd
+
+DEFAULT_PASSWORD: str = 'vyos'
+LOW_ENTROPY_MSG: str = 'should be at least 8 characters long;'
+WEAK_PASSWORD_MSG: str = 'The password complexity is too low - @MSG@'
+CRACKLIB_ERROR_MSG: str = 'A following error occurred: @MSG@\n' \
+ 'Possibly the cracklib database is corrupted or is missing. ' \
+ 'Try reinstalling the python3-cracklib package.'
+
+class EPasswdStrength(StrEnum):
+ WEAK = 'Weak'
+ DECENT = 'Decent'
+ STRONG = 'Strong'
+ ERROR = 'Cracklib Error'
+
+
+def calculate_entropy(charset: str, passwd: str) -> float:
+ """
+ Calculate the entropy of a password based on the set of characters used
+ Uses E = log2(R**L) formula, where
+ - R is the range (length) of the character set
+ - L is the length of password
+ """
+ return math.log(math.pow(len(charset), len(passwd)), 2)
+
+def evaluate_strength(passwd: str) -> dict[str, str]:
+ """ Evaluates password strength and returns a check result dict """
+ charset = (cracklib.ASCII_UPPERCASE + cracklib.ASCII_LOWERCASE +
+ string.punctuation + string.digits)
+
+ result = {
+ 'strength': '',
+ 'error': '',
+ }
+
+ try:
+ cracklib.FascistCheck(passwd)
+ except ValueError as e:
+ # The password is vulnerable to dictionary attack no matter the entropy
+ if 'is' in str(e):
+ msg = str(e).replace('is', 'should not be')
+ else:
+ msg = f'should not be {e}'
+ result.update(strength=EPasswdStrength.WEAK)
+ result.update(error=WEAK_PASSWORD_MSG.replace('@MSG@', msg))
+ except Exception as e:
+ result.update(strength=EPasswdStrength.ERROR)
+ result.update(error=CRACKLIB_ERROR_MSG.replace('@MSG@', str(e)))
+ else:
+ # Now check the password's entropy
+ # Cast to Decimal for more precise rounding
+ entropy = Decimal.from_float(calculate_entropy(charset, passwd))
+
+ match round(entropy):
+ case e if e in range(0, 59):
+ result.update(strength=EPasswdStrength.WEAK)
+ result.update(
+ error=WEAK_PASSWORD_MSG.replace('@MSG@', LOW_ENTROPY_MSG)
+ )
+ case e if e in range(60, 119):
+ result.update(strength=EPasswdStrength.DECENT)
+ case e if e >= 120:
+ result.update(strength=EPasswdStrength.STRONG)
+
+ return result
+
def make_password_hash(password):
""" Makes a password hash for /etc/shadow using mkpasswd """
diff --git a/python/vyos/utils/backend.py b/python/vyos/utils/backend.py
new file mode 100644
index 000000000..d302a2efd
--- /dev/null
+++ b/python/vyos/utils/backend.py
@@ -0,0 +1,94 @@
+# Copyright 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/>.
+
+# N.B. the following is a temporary addition for running smoketests under
+# vyconf and is not to be called explicitly, at the risk of catastophe.
+
+# pylint: disable=wrong-import-position
+
+from pathlib import Path
+
+from vyos.utils.io import ask_yes_no
+from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_active
+
+VYCONF_SENTINEL = '/run/vyconf_backend'
+
+MSG_ENABLE_VYCONF = 'This will enable the vyconf backend for testing. Proceed?'
+MSG_DISABLE_VYCONF = (
+ 'This will restore the legacy backend; it requires a reboot. Proceed?'
+)
+
+# read/set immutable file attribute without popen:
+# https://www.geeklab.info/2021/04/chattr-and-lsattr-in-python/
+import fcntl # pylint: disable=C0411 # noqa: E402
+from array import array # pylint: disable=C0411 # noqa: E402
+
+# FS constants - see /uapi/linux/fs.h in kernel source
+# or <elixir.free-electrons.com/linux/latest/source/include/uapi/linux/fs.h>
+FS_IOC_GETFLAGS = 0x80086601
+FS_IOC_SETFLAGS = 0x40086602
+FS_IMMUTABLE_FL = 0x010
+
+
+def chattri(filename: str, value: bool):
+ with open(filename, 'r') as f:
+ arg = array('L', [0])
+ fcntl.ioctl(f.fileno(), FS_IOC_GETFLAGS, arg, True)
+ if value:
+ arg[0] = arg[0] | FS_IMMUTABLE_FL
+ else:
+ arg[0] = arg[0] & ~FS_IMMUTABLE_FL
+ fcntl.ioctl(f.fileno(), FS_IOC_SETFLAGS, arg, True)
+
+
+def lsattri(filename: str) -> bool:
+ with open(filename, 'r') as f:
+ arg = array('L', [0])
+ fcntl.ioctl(f.fileno(), FS_IOC_GETFLAGS, arg, True)
+ return bool(arg[0] & FS_IMMUTABLE_FL)
+
+
+# End: read/set immutable file attribute without popen
+
+
+def vyconf_backend() -> bool:
+ return Path(VYCONF_SENTINEL).exists() and lsattri(VYCONF_SENTINEL)
+
+
+def set_vyconf_backend(value: bool, no_prompt: bool = False):
+ vyconfd_service = 'vyconfd.service'
+ commitd_service = 'vyos-commitd.service'
+ http_api_service = 'vyos-http-api.service'
+ match value:
+ case True:
+ if vyconf_backend():
+ return
+ if not no_prompt and not ask_yes_no(MSG_ENABLE_VYCONF):
+ return
+ Path(VYCONF_SENTINEL).touch()
+ chattri(VYCONF_SENTINEL, True)
+ call(f'systemctl restart {vyconfd_service}')
+ call(f'systemctl restart {commitd_service}')
+ if is_systemd_service_active(http_api_service):
+ call(f'systemctl restart {http_api_service}')
+ case False:
+ if not vyconf_backend():
+ return
+ if not no_prompt and not ask_yes_no(MSG_DISABLE_VYCONF):
+ return
+ chattri(VYCONF_SENTINEL, False)
+ Path(VYCONF_SENTINEL).unlink()
+ call('/sbin/shutdown -r now')
diff --git a/python/vyos/utils/boot.py b/python/vyos/utils/boot.py
index 708bef14d..f804cd94e 100644
--- a/python/vyos/utils/boot.py
+++ b/python/vyos/utils/boot.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/commit.py b/python/vyos/utils/commit.py
index 105aed8c2..4147c7fba 100644
--- a/python/vyos/utils/commit.py
+++ b/python/vyos/utils/commit.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -13,8 +13,13 @@
# 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/>.
+# pylint: disable=import-outside-toplevel
+
+from typing import IO
+
+
def commit_in_progress():
- """ Not to be used in normal op mode scripts! """
+ """Not to be used in normal op mode scripts!"""
# The CStore backend locks the config by opening a file
# The file is not removed after commit, so just checking
@@ -36,7 +41,9 @@ def commit_in_progress():
from vyos.defaults import commit_lock
if getuser() != 'root':
- raise OSError('This functions needs to be run as root to return correct results!')
+ raise OSError(
+ 'This functions needs to be run as root to return correct results!'
+ )
for proc in process_iter():
try:
@@ -45,7 +52,7 @@ def commit_in_progress():
for f in files:
if f.path == commit_lock:
return True
- except NoSuchProcess as err:
+ except NoSuchProcess:
# Process died before we could examine it
pass
# Default case
@@ -53,8 +60,71 @@ def commit_in_progress():
def wait_for_commit_lock():
- """ Not to be used in normal op mode scripts! """
+ """Not to be used in normal op mode scripts!"""
from time import sleep
+
# Very synchronous approach to multiprocessing
while commit_in_progress():
sleep(1)
+
+
+# For transitional compatibility with the legacy commit locking mechanism,
+# we require a lockf/fcntl (POSIX-type) lock, hence the following in place
+# of vyos.utils.locking
+
+
+def acquire_commit_lock_file() -> tuple[IO, str]:
+ import fcntl
+ from pathlib import Path
+ from vyos.defaults import commit_lock
+
+ try:
+ # pylint: disable=consider-using-with
+ lock_fd = Path(commit_lock).open('w')
+ except IOError as e:
+ out = f'Critical error opening commit lock file {e}'
+ return None, out
+
+ try:
+ fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ return lock_fd, ''
+ except IOError:
+ out = 'Configuration system locked by another commit in progress'
+ lock_fd.close()
+ return None, out
+
+
+def release_commit_lock_file(file_descr):
+ import fcntl
+
+ if file_descr is None:
+ return
+ fcntl.lockf(file_descr, fcntl.LOCK_UN)
+ file_descr.close()
+
+
+def call_commit_hooks(which: str):
+ import re
+ import os
+ from pathlib import Path
+ from vyos.defaults import commit_hooks
+ from vyos.utils.process import rc_cmd
+
+ if which not in list(commit_hooks):
+ raise ValueError(f'no entry {which} in commit_hooks')
+
+ hook_dir = commit_hooks[which]
+ file_list = list(Path(hook_dir).glob('*'))
+ regex = re.compile('^[a-zA-Z0-9._-]+$')
+ hook_list = sorted([str(f) for f in file_list if regex.match(f.name)])
+ err = False
+ out = ''
+ for runf in hook_list:
+ try:
+ e, o = rc_cmd(runf)
+ except FileNotFoundError:
+ continue
+ err = err | bool(e)
+ out = out + o
+
+ return out, int(err)
diff --git a/python/vyos/utils/config.py b/python/vyos/utils/config.py
index deda13c13..1f067e91e 100644
--- a/python/vyos/utils/config.py
+++ b/python/vyos/utils/config.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/configfs.py b/python/vyos/utils/configfs.py
index 8617f0129..307e1446c 100644
--- a/python/vyos/utils/configfs.py
+++ b/python/vyos/utils/configfs.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/convert.py b/python/vyos/utils/convert.py
index 2f587405d..ea07f9514 100644
--- a/python/vyos/utils/convert.py
+++ b/python/vyos/utils/convert.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/cpu.py b/python/vyos/utils/cpu.py
index 3bea5ac12..0f47123a4 100644
--- a/python/vyos/utils/cpu.py
+++ b/python/vyos/utils/cpu.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright 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
@@ -26,6 +26,7 @@ It has special cases for x86_64 and MAY work correctly on other architectures,
but nothing is certain.
"""
+import os
import re
def _read_cpuinfo():
@@ -99,3 +100,23 @@ def get_core_count():
core_count += 1
return core_count
+
+
+def get_available_cpus():
+ """ List of cpus with ids that are available in the system
+ Uses 'lscpu' command
+
+ Returns: list[dict[str, str | int | bool]]: cpus details
+ """
+ import json
+
+ from vyos.utils.process import cmd
+
+ out = json.loads(cmd('lscpu --extended -b --json'))
+
+ return out['cpus']
+
+
+def get_half_cpus():
+ """ return 1/2 of the numbers of available CPUs """
+ return max(1, os.cpu_count() // 2)
diff --git a/python/vyos/utils/dict.py b/python/vyos/utils/dict.py
index 1a7a6b96f..a43430eed 100644
--- a/python/vyos/utils/dict.py
+++ b/python/vyos/utils/dict.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -211,6 +211,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 (.).
"""
@@ -347,8 +380,6 @@ class FixedDict(dict):
raise ConfigError(f'Option "{k}" has no defined default')
"""
- from vyos import ConfigError
-
def __init__(self, **options):
self._allowed = options.keys()
super().__init__(**options)
@@ -368,7 +399,7 @@ class FixedDict(dict):
>>> d
{'key': 'value'}
"""
+ from vyos import ConfigError
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/disk.py b/python/vyos/utils/disk.py
index d4271ebe1..b822badde 100644
--- a/python/vyos/utils/disk.py
+++ b/python/vyos/utils/disk.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/error.py b/python/vyos/utils/error.py
index 8d4709bff..75ad813f3 100644
--- a/python/vyos/utils/error.py
+++ b/python/vyos/utils/error.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py
index eaebb57a3..1e2de2b39 100644
--- a/python/vyos/utils/file.py
+++ b/python/vyos/utils/file.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -28,22 +28,28 @@ def file_is_persistent(path):
absolute = os.path.abspath(os.path.dirname(path))
return re.match(location,absolute)
-def read_file(fname, defaultonfailure=None):
+def read_file(fname, defaultonfailure=None, sudo=False):
"""
read the content of a file, stripping any end characters (space, newlines)
should defaultonfailure be not None, it is returned on failure to read
"""
try:
- """ Read a file to string """
- with open(fname, 'r') as f:
- data = f.read().strip()
- return data
+ # Some files can only be read by root - emulate sudo cat call
+ if sudo:
+ from vyos.utils.process import cmd
+ data = cmd(['sudo', 'cat', fname])
+ else:
+ # If not sudo, just read the file
+ with open(fname, 'r') as f:
+ data = f.read()
+ return data.strip()
except Exception as e:
if defaultonfailure is not None:
return defaultonfailure
raise e
-def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None, append=False):
+def write_file(fname, data, defaultonfailure=None, user=None, group=None,
+ mode=None, append=False, trailing_newline=False):
"""
Write content of data to given fname, should defaultonfailure be not None,
it is returned on failure to read.
@@ -60,6 +66,9 @@ def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=N
bytes = 0
with open(fname, 'w' if not append else 'a') as f:
bytes = f.write(data)
+ if trailing_newline and not data.endswith('\n'):
+ f.write('\n')
+ bytes += 1
chown(fname, user, group)
chmod(fname, mode)
return bytes
@@ -83,36 +92,6 @@ def read_json(fname, defaultonfailure=None):
return defaultonfailure
raise e
-def chown(path, user=None, group=None, recursive=False):
- """ change file/directory owner """
- from pwd import getpwnam
- from grp import getgrnam
-
- if user is None and group is None:
- return False
-
- # path may also be an open file descriptor
- if not isinstance(path, int) and not os.path.exists(path):
- return False
-
- # keep current value if not specified otherwise
- uid = -1
- gid = -1
-
- if user:
- uid = getpwnam(user).pw_uid
- if group:
- gid = getgrnam(group).gr_gid
-
- if recursive:
- for dirpath, dirnames, filenames in os.walk(path):
- os.chown(dirpath, uid, gid)
- for filename in filenames:
- os.chown(os.path.join(dirpath, filename), uid, gid)
- else:
- os.chown(path, uid, gid)
- return True
-
def chmod(path, bitmask):
# path may also be an open file descriptor
@@ -166,12 +145,6 @@ def file_permissions(path):
""" Return file permissions in string format, e.g '0755' """
return oct(os.stat(path).st_mode)[4:]
-def makedir(path, user=None, group=None):
- if os.path.exists(path):
- return
- os.makedirs(path, mode=0o755)
- chown(path, user, group)
-
def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sleep_interval=0.1):
""" Waits for an inotify event to occur """
if not os.path.dirname(file_path):
diff --git a/python/vyos/utils/io.py b/python/vyos/utils/io.py
index 205210b66..0883376d1 100644
--- a/python/vyos/utils/io.py
+++ b/python/vyos/utils/io.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/kernel.py b/python/vyos/utils/kernel.py
index 847f80108..4d8544670 100644
--- a/python/vyos/utils/kernel.py
+++ b/python/vyos/utils/kernel.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -15,6 +15,10 @@
import os
+# A list of used Kernel constants
+# https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/net/wireguard/messages.h?h=linux-6.6.y#n45
+WIREGUARD_REKEY_AFTER_TIME = 120
+
def check_kmod(k_mod):
""" Common utility function to load required kernel modules on demand """
from vyos import ConfigError
diff --git a/python/vyos/utils/list.py b/python/vyos/utils/list.py
index 63ef720ab..931084e7c 100644
--- a/python/vyos/utils/list.py
+++ b/python/vyos/utils/list.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/locking.py b/python/vyos/utils/locking.py
index 63cb1a816..f4cd6fd41 100644
--- a/python/vyos/utils/locking.py
+++ b/python/vyos/utils/locking.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/misc.py b/python/vyos/utils/misc.py
index ac8011b8d..c7f7e7343 100644
--- a/python/vyos/utils/misc.py
+++ b/python/vyos/utils/misc.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -48,11 +48,11 @@ def install_into_config(conf, config_paths, override_prompt=True):
for path in config_paths:
if override_prompt and conf.exists(path) and not conf.is_multi(path):
- if not ask_yes_no(f'Config node "{node}" already exists. Do you want to overwrite it?'):
+ if not ask_yes_no(f'Config node "{path}" already exists. Do you want to overwrite it?'):
continue
try:
- cmd(f'/usr/libexec/vyos/vyconf/vy_set {path}')
+ cmd(f'/opt/vyatta/sbin/my_set {path}')
count += 1
except:
failed.append(path)
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index dc0c0a6d6..93f780d8b 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -13,9 +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/>.
+from socket import AF_INET
+from socket import AF_INET6
+
def _are_same_ip(one, two):
- from socket import AF_INET
- from socket import AF_INET6
from socket import inet_pton
from vyos.template import is_ipv4
# compare the binary representation of the IP
@@ -80,7 +81,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'
@@ -256,40 +260,60 @@ def mac2eui64(mac, prefix=None):
except: # pylint: disable=bare-except
return
-def check_port_availability(ipaddress, port, protocol):
+def check_port_availability(address: str=None, port: int=0, protocol: str='tcp') -> bool:
"""
- Check if port is available and not used by any service
- Return False if a port is busy or IP address does not exists
+ Check if given port is available and not used by any service.
+
Should be used carefully for services that can start listening
dynamically, because IP address may be dynamic too
+
+ Args:
+ address: IPv4 or IPv6 address - if None, checks on all interfaces
+ port: TCP/UDP port number.
+
+
+ Returns:
+ False if a port is busy or IP address does not exists
+ True if a port is free and IP address exists
"""
- from socketserver import TCPServer, UDPServer
+ import socket
from ipaddress import ip_address
+ # treat None as "any address"
+ address = address or '::'
+
# verify arguments
try:
- ipaddress = ip_address(ipaddress).compressed
- except:
- raise ValueError(f'The {ipaddress} is not a valid IPv4 or IPv6 address')
+ address = ip_address(address).compressed
+ except ValueError:
+ raise ValueError(f'{address} is not a valid IPv4 or IPv6 address')
if port not in range(1, 65536):
- raise ValueError(f'The port number {port} is not in the 1-65535 range')
+ raise ValueError(f'Port {port} is not in range 1-65535')
if protocol not in ['tcp', 'udp']:
- raise ValueError(f'The protocol {protocol} is not supported. Only tcp and udp are allowed')
+ raise ValueError(f'{protocol} is not supported - only tcp and udp are allowed')
- # check port availability
+ protocol = socket.SOCK_STREAM if protocol == 'tcp' else socket.SOCK_DGRAM
try:
- if protocol == 'tcp':
- server = TCPServer((ipaddress, port), None, bind_and_activate=True)
- if protocol == 'udp':
- server = UDPServer((ipaddress, port), None, bind_and_activate=True)
- server.server_close()
- except Exception as e:
- # errno.h:
- #define EADDRINUSE 98 /* Address already in use */
- if e.errno == 98:
+ addr_info = socket.getaddrinfo(address, port, socket.AF_UNSPEC, protocol)
+ except socket.gaierror as e:
+ print(f'Invalid address: {address}')
+ return False
+
+ for family, socktype, proto, canonname, sockaddr in addr_info:
+ try:
+ with socket.socket(family, socktype, proto) as s:
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind(sockaddr)
+ # port is free to use
+ return True
+ except OSError:
+ # port is already in use
return False
- return True
+ # if we reach this point, no socket was tested and we assume the port is
+ # already in use - better safe then sorry
+ return False
+
def is_listen_port_bind_service(port: int, service: str) -> bool:
"""Check if listen port bound to expected program name
@@ -327,7 +351,7 @@ def is_ipv6_link_local(addr):
def is_addr_assigned(ip_address, vrf=None, return_ifname=False, include_vrf=False) -> bool | str:
""" Verify if the given IPv4/IPv6 address is assigned to any interface """
- from netifaces import interfaces
+ from netifaces import interfaces # pylint: disable = no-name-in-module
from vyos.utils.network import get_interface_config
from vyos.utils.dict import dict_search
@@ -396,6 +420,21 @@ def is_wireguard_key_pair(private_key: str, public_key:str) -> bool:
else:
return False
+def get_wireguard_peers(ifname: str) -> list:
+ """
+ Return list of configured Wireguard peers for interface
+ :param ifname: Interface name
+ :type ifname: str
+ :return: list of public keys
+ :rtype: list
+ """
+ if not interface_exists(ifname):
+ return []
+
+ from vyos.utils.process import cmd
+ peers = cmd(f'wg show {ifname} peers')
+ return peers.splitlines()
+
def is_subnet_connected(subnet, primary=False):
"""
Verify is the given IPv4/IPv6 subnet is connected to any interface on this
@@ -410,10 +449,8 @@ def is_subnet_connected(subnet, primary=False):
from ipaddress import ip_address
from ipaddress import ip_network
- from netifaces import ifaddresses
- from netifaces import interfaces
- from netifaces import AF_INET
- from netifaces import AF_INET6
+ from netifaces import ifaddresses # pylint: disable = no-name-in-module
+ from netifaces import interfaces # pylint: disable = no-name-in-module
from vyos.template import is_ipv6
@@ -447,9 +484,7 @@ def is_subnet_connected(subnet, primary=False):
def is_afi_configured(interface: str, afi):
""" Check if given address family is configured, or in other words - an IP
address is assigned to the interface. """
- from netifaces import ifaddresses
- from netifaces import AF_INET
- from netifaces import AF_INET6
+ from netifaces import ifaddresses # pylint: disable = no-name-in-module
if afi not in [AF_INET, AF_INET6]:
raise ValueError('Address family must be in [AF_INET, AF_INET6]')
@@ -599,3 +634,35 @@ def get_nft_vrf_zone_mapping() -> dict:
for (vrf_name, vrf_id) in vrf_list:
output.append({'interface' : vrf_name, 'vrf_tableid' : vrf_id})
return output
+
+def is_valid_ipv4_address_or_range(addr: str) -> bool:
+ """
+ Validates if the provided address is a valid IPv4, CIDR or IPv4 range
+ :param addr: address to test
+ :return: bool: True if provided address is valid
+ """
+ from ipaddress import ip_network
+ try:
+ if '-' in addr: # If we are checking a range, validate both address's individually
+ split = addr.split('-')
+ return is_valid_ipv4_address_or_range(split[0]) and is_valid_ipv4_address_or_range(split[1])
+ else:
+ return ip_network(addr).version == 4
+ except:
+ return False
+
+def is_valid_ipv6_address_or_range(addr: str) -> bool:
+ """
+ Validates if the provided address is a valid IPv4, CIDR or IPv4 range
+ :param addr: address to test
+ :return: bool: True if provided address is valid
+ """
+ from ipaddress import ip_network
+ try:
+ if '-' in addr: # If we are checking a range, validate both address's individually
+ split = addr.split('-')
+ return is_valid_ipv6_address_or_range(split[0]) and is_valid_ipv6_address_or_range(split[1])
+ else:
+ return ip_network(addr).version == 6
+ except:
+ return False
diff --git a/python/vyos/utils/permission.py b/python/vyos/utils/permission.py
index d938b494f..08bb7bdbc 100644
--- a/python/vyos/utils/permission.py
+++ b/python/vyos/utils/permission.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -15,23 +15,37 @@
import os
-def chown(path, user, group):
+def chown(path, user=None, group=None, recursive=False):
""" change file/directory owner """
from pwd import getpwnam
from grp import getgrnam
- if user is None or group is None:
+ if user is None and group is None:
return False
# path may also be an open file descriptor
if not isinstance(path, int) and not os.path.exists(path):
return False
- uid = getpwnam(user).pw_uid
- gid = getgrnam(group).gr_gid
- os.chown(path, uid, gid)
+ # keep current value if not specified otherwise
+ uid = -1
+ gid = -1
+
+ if user:
+ uid = getpwnam(user).pw_uid
+ if group:
+ gid = getgrnam(group).gr_gid
+
+ if recursive:
+ for dirpath, dirnames, filenames in os.walk(path):
+ os.chown(dirpath, uid, gid)
+ for filename in filenames:
+ os.chown(os.path.join(dirpath, filename), uid, gid)
+ else:
+ os.chown(path, uid, gid)
return True
+
def chmod(path, bitmask):
# path may also be an open file descriptor
if not isinstance(path, int) and not os.path.exists(path):
diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py
index 054088325..86a2747af 100644
--- a/python/vyos/utils/process.py
+++ b/python/vyos/utils/process.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import shlex
from subprocess import Popen
from subprocess import PIPE
@@ -21,20 +22,17 @@ from subprocess import STDOUT
from subprocess import DEVNULL
-def get_wrapper(vrf, netns, auth):
- wrapper = ''
+def get_wrapper(vrf, netns):
+ wrapper = None
if vrf:
- wrapper = f'ip vrf exec {vrf} '
+ wrapper = ['ip', 'vrf', 'exec', vrf]
elif netns:
- wrapper = f'ip netns exec {netns} '
- if auth:
- wrapper = f'{auth} {wrapper}'
+ wrapper = ['ip', 'netns', 'exec', netns]
return wrapper
def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=PIPE, decode='utf-8', auth='', vrf=None,
- netns=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8', vrf=None, netns=None):
"""
popen is a wrapper helper around subprocess.Popen
with it default setting it will return a tuple (out, err)
@@ -75,28 +73,33 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
if not debug.enabled(flag):
flag = 'command'
+ use_shell = shell
+ stdin = None
+ if shell is None:
+ use_shell = False
+ if ' ' in command:
+ use_shell = True
+ if env:
+ use_shell = True
+
# Must be run as root to execute command in VRF or network namespace
+ wrapper = get_wrapper(vrf, netns)
if vrf or netns:
if os.getuid() != 0:
raise OSError(
'Permission denied: cannot execute commands in VRF and netns contexts as an unprivileged user'
)
- wrapper = get_wrapper(vrf, netns, auth)
- command = f'{wrapper} {command}'
+ if use_shell:
+ command = f'{shlex.join(wrapper)} {command}'
+ else:
+ if type(command) is not list:
+ command = [command]
+ command = wrapper + command
- cmd_msg = f"cmd '{command}'"
+ cmd_msg = f"cmd '{command}'" if use_shell else f"cmd '{shlex.join(command)}'"
debug.message(cmd_msg, flag)
- use_shell = shell
- stdin = None
- if shell is None:
- use_shell = False
- if ' ' in command:
- use_shell = True
- if env:
- use_shell = True
-
if input:
stdin = PIPE
input = input.encode() if type(input) is str else input
@@ -155,7 +158,7 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None,
def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=PIPE, stderr=PIPE, decode='utf-8', raising=None, message='',
- expect=[0], auth='', vrf=None, netns=None):
+ expect=[0], vrf=None, netns=None):
"""
A wrapper around popen, which returns the stdout and
will raise the error code of a command
@@ -171,12 +174,11 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
input=input, timeout=timeout,
env=env, shell=shell,
decode=decode,
- auth=auth,
vrf=vrf,
netns=netns,
)
if code not in expect:
- wrapper = get_wrapper(vrf, netns, auth='')
+ wrapper = get_wrapper(vrf, netns)
command = f'{wrapper} {command}'
feedback = message + '\n' if message else ''
feedback += f'failed to run command: {command}\n'
diff --git a/python/vyos/utils/serial.py b/python/vyos/utils/serial.py
index b646f881e..36b483e91 100644
--- a/python/vyos/utils/serial.py
+++ b/python/vyos/utils/serial.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -116,3 +116,16 @@ def restart_login_consoles(prompt_user=False, quiet=True, devices: List[str]=[])
cmd(f'systemctl stop {unit_name}')
return True
+
+def is_tty(name: str) -> bool:
+ """ Check if a given device file (e.g. /dev/ttyS0) is a TTY (teletypewriter)
+ device in Linux
+ """
+ import os
+ path_tty = f'/dev/{name}'
+ if os.path.exists(path_tty):
+ with open(path_tty, 'rb') as f:
+ fd = f.fileno()
+ # True if filename is a TTY
+ return os.isatty(fd)
+ return False
diff --git a/python/vyos/utils/session.py b/python/vyos/utils/session.py
new file mode 100644
index 000000000..bc5240fc7
--- /dev/null
+++ b/python/vyos/utils/session.py
@@ -0,0 +1,25 @@
+# Copyright 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/>.
+
+# pylint: disable=import-outside-toplevel
+
+
+def in_config_session():
+ """Vyatta bash completion uses the following environment variable for
+ indication of the config mode environment, independent of legacy backend
+ initialization of Cstore"""
+ from os import environ
+
+ return '_OFR_CONFIGURE' in environ
diff --git a/python/vyos/utils/strip_config.py b/python/vyos/utils/strip_config.py
index 7a9c78c9f..17f6867cb 100644
--- a/python/vyos/utils/strip_config.py
+++ b/python/vyos/utils/strip_config.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
#
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/system.py b/python/vyos/utils/system.py
index 6c112334b..e2197daf2 100644
--- a/python/vyos/utils/system.py
+++ b/python/vyos/utils/system.py
@@ -1,4 +1,4 @@
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/utils/vti_updown_db.py b/python/vyos/utils/vti_updown_db.py
index b491fc6f2..f4dd24007 100644
--- a/python/vyos/utils/vti_updown_db.py
+++ b/python/vyos/utils/vti_updown_db.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/version.py b/python/vyos/version.py
index 86e96d0ec..01986e4da 100644
--- a/python/vyos/version.py
+++ b/python/vyos/version.py
@@ -1,4 +1,4 @@
-# Copyright 2017-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/vpp/__init__.py b/python/vyos/vpp/__init__.py
new file mode 100644
index 000000000..3352914f1
--- /dev/null
+++ b/python/vyos/vpp/__init__.py
@@ -0,0 +1,20 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from .control_vpp import VPPControl
+
+__all__ = ['VPPControl']
diff --git a/python/vyos/vpp/acl/__init__.py b/python/vyos/vpp/acl/__init__.py
new file mode 100644
index 000000000..6bd6cdb23
--- /dev/null
+++ b/python/vyos/vpp/acl/__init__.py
@@ -0,0 +1,3 @@
+from .acl import Acl
+
+__all__ = ['Acl']
diff --git a/python/vyos/vpp/acl/acl.py b/python/vyos/vpp/acl/acl.py
new file mode 100644
index 000000000..57d36ac91
--- /dev/null
+++ b/python/vyos/vpp/acl/acl.py
@@ -0,0 +1,106 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+
+NO_ACL_INDEX = 0xFFFFFFFF
+
+
+class Acl:
+ def __init__(self):
+ self.vpp = VPPControl()
+
+ def get_acl_index_by_tag(self, tag):
+ """Get ACL index by tag name
+ https://github.com/FDio/vpp/blob/21c641f9356da5137760cdc799127064c8c1fd31/src/plugins/acl/acl.api
+ """
+ for acl in self.vpp.api.acl_dump(acl_index=NO_ACL_INDEX):
+ if acl.tag == tag:
+ return acl.acl_index
+ return NO_ACL_INDEX
+
+ def add_replace_acl(self, tag, rules):
+ """Add new ACL or replace existing one"""
+ self.vpp.api.acl_add_replace(
+ tag=tag,
+ acl_index=self.get_acl_index_by_tag(tag),
+ count=len(rules),
+ r=rules,
+ )
+
+ def delete_acl(self, tag):
+ """Delete existing ACL"""
+ self.vpp.api.acl_del(acl_index=self.get_acl_index_by_tag(tag))
+
+ def add_acl_interface(self, interface, input_tags, output_tags):
+ """Add or replace ACLs on interface"""
+ acls = []
+ for tag in input_tags:
+ acl_index = self.get_acl_index_by_tag(tag)
+ acls.append(acl_index)
+ for tag in output_tags:
+ acl_index = self.get_acl_index_by_tag(tag)
+ acls.append(acl_index)
+ self.vpp.api.acl_interface_set_acl_list(
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ count=len(acls),
+ n_input=len(input_tags),
+ acls=acls,
+ )
+
+ def delete_acl_interface(self, interface):
+ """Delete ACLs from interface"""
+ self.vpp.api.acl_interface_set_acl_list(
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ count=0,
+ )
+
+ def get_macip_acl_index_by_tag(self, tag):
+ """Get macip ACL by tag name"""
+ for acl in self.vpp.api.macip_acl_dump():
+ if acl.tag == tag:
+ return acl.acl_index
+ return NO_ACL_INDEX
+
+ def add_replace_acl_macip(self, tag, rules):
+ """Add or replace existing macip ACL"""
+ self.vpp.api.macip_acl_add_replace(
+ tag=tag,
+ acl_index=self.get_macip_acl_index_by_tag(tag),
+ count=len(rules),
+ r=rules,
+ )
+
+ def delete_acl_macip(self, tag):
+ """Delete existing macip ACL"""
+ self.vpp.api.macip_acl_del(acl_index=self.get_macip_acl_index_by_tag(tag))
+
+ def add_acl_macip_interface(self, interface, tag):
+ """Add or replace macip ACLs on interface"""
+ self.vpp.api.macip_acl_interface_add_del(
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ acl_index=self.get_macip_acl_index_by_tag(tag),
+ is_add=True,
+ )
+
+ def delete_acl_macip_interface(self, interface):
+ """Delete macip ACLs from interface"""
+ self.vpp.api.macip_acl_interface_add_del(
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ is_add=False,
+ )
diff --git a/python/vyos/vpp/config_deps.py b/python/vyos/vpp/config_deps.py
new file mode 100644
index 000000000..dffd865d1
--- /dev/null
+++ b/python/vyos/vpp/config_deps.py
@@ -0,0 +1,76 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+def deps_xconnect_dict(conf) -> dict[str, list[str]]:
+ """Get a dict of all xconnect interface members:
+
+ keys: members
+
+ values: xconnect interfaces
+
+ Args:
+ conf (config): VyOS config object
+
+ Returns:
+ dict[str, list[str]]: dict of members
+ """
+ xconn_members_dict: dict[str, list[str]] = {}
+ config = conf.get_config_dict(
+ ['vpp', 'interfaces', 'xconnect'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ for xconn_name, xconn_config in config.items():
+ for member_name in xconn_config.get('member', {}).get('interface', []):
+ xconn_ifaces_list = xconn_members_dict.get(xconn_name, [])
+ xconn_ifaces_list.append(xconn_name)
+ xconn_members_dict.update({member_name: xconn_ifaces_list})
+
+ return xconn_members_dict
+
+
+def deps_bridge_dict(conf) -> dict[str, list[str]]:
+ """Get a dict of all bridge interface members:
+
+ keys: members
+
+ values: bridge interfaces
+
+ Args:
+ conf (config): VyOS config object
+
+ Returns:
+ dict[str, list[str]]: dict of members
+ """
+ bridge_members_dict: dict[str, list[str]] = {}
+ config = conf.get_config_dict(
+ ['vpp', 'interfaces', 'bridge'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+
+ for bridge_name, bridge_config in config.items():
+ for member_name in bridge_config.get('member', {}).get('interface', []):
+ bridge_ifaces_list = bridge_members_dict.get(bridge_name, [])
+ bridge_ifaces_list.append(bridge_name)
+ bridge_members_dict.update({member_name: bridge_ifaces_list})
+
+ return bridge_members_dict
diff --git a/python/vyos/vpp/config_filter.py b/python/vyos/vpp/config_filter.py
new file mode 100644
index 000000000..dc864930b
--- /dev/null
+++ b/python/vyos/vpp/config_filter.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.config import Config
+
+
+def iface_filter_eth(config: Config, iface: str) -> None:
+ """Filter out unsupported config nodes from Ethernet interface config
+
+ Args:
+ config (Config): config object
+ iface (str): Ethernet interface name to filter
+ """
+ allowed_nodes: list[str] = [
+ 'address',
+ 'description',
+ 'dhcp-options',
+ 'dhcpv6-options',
+ 'disable',
+ 'eapol',
+ 'hw-id',
+ 'ip',
+ 'ipv6',
+ 'mtu',
+ 'redirect',
+ 'vif',
+ 'vif-s',
+ 'vrf',
+ ]
+
+ # get list of config nides in a session configuration
+ iface_nodes = config._session_config.list_nodes(['interfaces', 'ethernet', iface])
+
+ # clean cached session config
+ if False in config._dict_cache:
+ del config._dict_cache[False]
+
+ # remove unsupported config nodes
+ for cfg_node in iface_nodes:
+ if cfg_node not in allowed_nodes:
+ config._session_config.delete(['interfaces', 'ethernet', iface, cfg_node])
+ print(
+ f'WARNING: {cfg_node} option in {iface} settings is not supported by VPP interfaces. It will be ignored.'
+ )
diff --git a/python/vyos/vpp/config_resource_checks/__init__.py b/python/vyos/vpp/config_resource_checks/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/vpp/config_resource_checks/__init__.py
diff --git a/python/vyos/vpp/config_resource_checks/cpu.py b/python/vyos/vpp/config_resource_checks/cpu.py
new file mode 100644
index 000000000..b319608e4
--- /dev/null
+++ b/python/vyos/vpp/config_resource_checks/cpu.py
@@ -0,0 +1,76 @@
+# Used for validating estimated CPU/physical cores use
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.utils.cpu import get_available_cpus, get_core_count
+
+from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map
+
+
+# Get default value for reserved cpu cores
+reserved_cpus = default_resource_map.get('reserved_cpu_cores')
+
+
+def available_cores_count(cpu_settings: dict) -> int:
+ core_count = get_core_count()
+
+ if cpu_settings.get('main_core'):
+ core_count -= 1
+
+ skip_cores = int(cpu_settings.get('skip_cores', 0))
+ # The default settings assume that
+ # at least 2 CPU cores should remain reserved for system use
+ # (only in case of current runtime is not smoke test)
+ if skip_cores < reserved_cpus:
+ core_count -= reserved_cpus
+ else:
+ core_count -= skip_cores
+
+ return core_count
+
+
+def available_cores_list(skip_cores: int) -> list:
+ # Available cores are all CPU cores without first N skipped cores that will not be used
+ # Get all available physical cores - use set to filter out unique values
+ cpu_cores = set(map(lambda el: el['cpu'], get_available_cpus()))
+ cpu_cores = list(cpu_cores)
+
+ return cpu_cores[skip_cores:]
+
+
+def worker_cores_list(iface: str, worker_ranges: list) -> list:
+ all_core_numbers = []
+ for worker_range in worker_ranges:
+ core_numbers = worker_range.split('-')
+
+ if int(core_numbers[0]) > int(core_numbers[-1]):
+ raise ValueError(
+ f'Range for "{iface} workers {worker_range}" is not correct'
+ )
+
+ all_core_numbers.extend(range(int(core_numbers[0]), int(core_numbers[-1]) + 1))
+
+ # Check for duplicates
+ duplicates = set(
+ [str(x) for n, x in enumerate(all_core_numbers) if x in all_core_numbers[:n]]
+ )
+ if duplicates:
+ raise ValueError(
+ f'Some workers in "{iface} workers" are duplicated: #{",".join(list(duplicates))}'
+ )
+
+ return all_core_numbers
diff --git a/python/vyos/vpp/config_resource_checks/memory.py b/python/vyos/vpp/config_resource_checks/memory.py
new file mode 100644
index 000000000..9391e21e1
--- /dev/null
+++ b/python/vyos/vpp/config_resource_checks/memory.py
@@ -0,0 +1,189 @@
+# Used for memory consumption calculations
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# 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 (
+ human_memory_to_bytes,
+ human_page_memory_to_bytes,
+)
+from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map
+
+
+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
+ retrieved from /sys/kernel/mm/hugepages
+ """
+ base_path = '/sys/kernel/mm/hugepages'
+ info = {}
+
+ 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_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 memory currently used by VPP in bytes (RSS value)
+ """
+ try:
+ out = cmd('ps -o rss= -p $(pidof vpp)')
+ except OSError:
+ out = 0
+ return int(out) << 10
+
+
+def get_numa_count():
+ """
+ Run `numactl --hardware` and parse the 'available:' line.
+ """
+ out = cmd('numactl --hardware')
+ # e.g. "available: 2 nodes (0-1)"
+ m = re.search(r'available:\s*(\d+)\s+nodes', out)
+ 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(
+ settings.get('buffers', {}).get(
+ 'buffers_per_numa', default_resource_map.get('buffers_per_numa')
+ )
+ )
+ data_size = int(
+ settings.get('buffers', {}).get(
+ 'data_size', default_resource_map.get('data_size')
+ )
+ )
+ buffers_memory = buffers_per_numa * data_size * numa_count
+ return buffers_memory
+
+
+def main_heap_page_size(settings: dict) -> int:
+ heap_page_size = settings.get('memory', {}).get(
+ 'main_heap_page_size', default_resource_map.get('main_heap_page_size')
+ )
+ return human_page_memory_to_bytes(heap_page_size)
+
+
+def memory_main_heap(settings: dict) -> int:
+ heap_size = settings.get('memory', {}).get(
+ 'main_heap_size', default_resource_map.get('main_heap_size')
+ )
+ return human_memory_to_bytes(heap_size)
+
+
+def ipv6_heap_size(settings: dict) -> int:
+ heap_size = settings.get('ipv6', {}).get(
+ 'heap_size', default_resource_map.get('ipv6_heap_size')
+ )
+ return human_memory_to_bytes(heap_size)
+
+
+def total_heap_size(heap_size: int, heap_page_size: int) -> int:
+ return (heap_size + heap_page_size - 1) & ~(heap_page_size - 1)
+
+
+def statseg_size(settings: dict) -> int:
+ statseg_memory = settings.get('statseg', {}).get(
+ 'size', default_resource_map.get('statseg_heap_size')
+ )
+ return human_memory_to_bytes(statseg_memory)
+
+
+def statseg_page_size(settings: dict) -> int:
+ page_size = settings.get('statseg', {}).get('page_size')
+ return human_page_memory_to_bytes(page_size)
+
+
+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) -> dict:
+ memory = {'2M': 0, '1G': 0, '4K': 0}
+
+ mem_stats = {
+ '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),
+ ),
+ main_heap_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), 0),
+ }
+
+ for memory_size, page_size in mem_stats.values():
+ memory[classify_page_size(page_size)] += memory_size
+
+ return memory
diff --git a/python/vyos/vpp/config_resource_checks/resource_defaults.py b/python/vyos/vpp/config_resource_checks/resource_defaults.py
new file mode 100644
index 000000000..d8316fcaf
--- /dev/null
+++ b/python/vyos/vpp/config_resource_checks/resource_defaults.py
@@ -0,0 +1,43 @@
+# Default values for resource consumption checks
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+default_resource_map = {
+ # Default amount of buffers per NUMA (populated CPU socket)
+ 'buffers_per_numa': 16384,
+ # Default size of buffer (in bytes)
+ 'data_size': 2048,
+ # Default hugepage size for VPP
+ 'hugepage_size': '2M',
+ # Default amount of memory allocated for VPP exclusive usage
+ 'main_heap_size': '3G',
+ # Default main heap page size
+ 'main_heap_page_size': '2M',
+ # Default size of buffers transferred via netlink
+ 'netlink_rx_buffer_size': 212992,
+ # Default amount of memory allocated for VPP stats segment usage
+ 'statseg_heap_size': '128M',
+ # Minimal amount of memory required to start VPP
+ 'min_memory': '8G',
+ # Minimal number of physical CPU cores required to start VPP
+ 'min_cpus': 4,
+ # Reserve at least 2 physical cores
+ 'reserved_cpu_cores': 2,
+ # Default heap size for IPv6
+ 'ipv6_heap_size': '32M',
+}
diff --git a/python/vyos/vpp/config_verify.py b/python/vyos/vpp/config_verify.py
new file mode 100644
index 000000000..cf0b923bb
--- /dev/null
+++ b/python/vyos/vpp/config_verify.py
@@ -0,0 +1,444 @@
+# Used for verifying configuration vpp interfaces
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import psutil
+
+from vyos import ConfigError
+from vyos.utils.cpu import get_core_count as total_core_count
+
+from vyos.vpp.control_host import get_eth_driver
+from vyos.vpp.config_resource_checks import cpu as cpu_checks, memory as mem_checks
+from vyos.vpp.config_resource_checks.resource_defaults import default_resource_map
+from vyos.vpp.utils import human_memory_to_bytes, bytes_to_human_memory
+
+
+def verify_vpp_remove_kernel_interface(config: dict):
+ """Common verify for removed kernel-interfaces.
+ Verify that removed kernel interface are not used in 'vpp kernel-interfaces'.
+
+ Example:
+ delete vpp interfaces gre|vxlan <tag>X kernel-interface vpp-tunX
+ set vpp kernel-interface vpp-tunX
+ """
+ if (
+ 'remove' in config
+ and 'kernel_interface_removed' in config
+ and 'vpp_kernel_interfaces' in config
+ ):
+ removed_interfaces = config['kernel_interface_removed']
+ used_interfaces = config['vpp_kernel_interfaces']
+
+ for interface in removed_interfaces:
+ if interface in used_interfaces:
+ raise ConfigError(
+ f'"{interface}" is still in use within "vpp kernel-interfaces". '
+ 'Please remove it before proceeding.'
+ )
+
+
+def verify_vpp_change_kernel_interface(config: dict):
+ """Common verify for changed kernel-interface
+
+ Example:
+ set vpp interfaces gre|vxlan <tag> kernel-interface vpp-tunX'
+ commit
+ set vpp interfaces gre|vxlan <tag> kernel-interface vpp-tunY'
+ commit
+
+ check if we have kernel interface config 'vpp kernel-interface vpp-tunX'
+ """
+ kernel_interface_removed = config.get('kernel_interface_removed', [])
+ vpp_kernel_interfaces = config.get('vpp_kernel_interfaces', {})
+
+ for interface in kernel_interface_removed:
+ if interface in vpp_kernel_interfaces:
+ raise ConfigError(
+ f'interface "{interface}" is still in use within "vpp kernel-interfaces". '
+ f'Please remove it "vpp kernel-interface {interface}" before proceeding.'
+ )
+
+
+def verify_vpp_exists_kernel_interface(config: dict):
+ """Verify is a kernel-interface already created by another VPP LCP pair
+
+ Example:
+ set vpp interfaces vxlan vxlan10 kernel-interface vpp-tun10'
+ commit
+ set vpp interfaces vxlan vxlan20 kernel-interface vpp-tun10'
+ commit
+ """
+ kernel_interface = config.get('kernel_interface', '')
+ vpp_interface = config.get('ifname', '')
+ candidate_kernel_interfaces = config.get('candidate_kernel_interfaces', [])
+
+ for candidate_kernel_iface in candidate_kernel_interfaces:
+ if (
+ vpp_interface != candidate_kernel_iface[0]
+ and kernel_interface == candidate_kernel_iface[1]
+ ):
+ raise ConfigError(
+ f'Kernel interface "{kernel_interface}" is already configured for {candidate_kernel_iface[0]}. '
+ 'Duplicates are not allowed.'
+ )
+
+
+def verify_vpp_remove_xconnect_interface(config: dict):
+ if not config.get('remove'):
+ return
+ for xconn_member, xconn_iface in config.get('xconn_members').items():
+ if xconn_member == config.get('ifname'):
+ raise ConfigError(
+ f'interface "{xconn_member}" is still in use within "vpp interfaces xconnect". '
+ f'Please remove it from "vpp interface xconnect {xconn_iface}" before proceeding.'
+ )
+
+
+def verify_vpp_tunnel_source_address(config: dict):
+ from vyos.utils.network import is_intf_addr_assigned
+
+ address = config.get('source_address')
+ for iface in config.get('vpp_ether_vif_ifaces', []):
+ if is_intf_addr_assigned(iface, address):
+ return True
+
+ raise ConfigError(
+ f'Source address "{address}" is not assigned on any Ethernet or VIF interface!'
+ )
+
+
+def verify_dev_driver(iface_name: str, driver_type: str) -> bool:
+ # Lists of drivers compatible with DPDK and XDP
+ drivers_dpdk: list[str] = [
+ 'atlantic',
+ 'bnx2x',
+ 'e1000',
+ 'ena',
+ 'gve',
+ 'hv_netvsc',
+ 'i40e',
+ 'ice',
+ 'igc',
+ 'ixgbe',
+ 'liquidio',
+ 'mlx4_core',
+ 'mlx5_core',
+ 'qede',
+ 'sfc',
+ 'tap',
+ 'tun',
+ 'virtio_net',
+ 'vmxnet3',
+ ]
+
+ drivers_xdp: list[str] = [
+ 'atlantic',
+ 'ena',
+ 'gve',
+ 'hv_netvsc',
+ 'i40e',
+ 'ice',
+ 'igb',
+ 'igc',
+ 'ixgbe',
+ 'mlx4_core',
+ 'mlx5_core',
+ 'qede',
+ 'sfc',
+ 'tap',
+ 'tun',
+ 'virtio_net',
+ 'vmxnet3',
+ ]
+
+ driver: str = get_eth_driver(iface_name)
+
+ if driver_type == 'dpdk':
+ if driver in drivers_dpdk:
+ return True
+ elif driver_type == 'xdp':
+ if driver in drivers_xdp:
+ return True
+ else:
+ raise ConfigError(f'"Driver type {driver_type} is wrong')
+
+ return False
+
+
+def verify_vpp_minimum_cpus():
+ """
+ Verify that the host system has enough physical CPU cores
+ Current minimal requirement is 4
+ """
+ min_cpus = default_resource_map.get('min_cpus')
+ if total_core_count() < min_cpus:
+ raise ConfigError(
+ 'This system does not meet minimal requirements for VPP. '
+ f'Minimum {min_cpus} CPU cores are required.'
+ )
+
+
+def verify_vpp_minimum_memory():
+ """
+ Verify that the host system has enough RAM
+ Calculate by retrieving the amount of physical memory
+ And the minimal requirement (currently 8 GB). Round before comparing -
+ To avoid situations like when a machine nominally has 8192 MB (8 giga/gibibytes)
+ But the OS sees only 7.75 GB, creating a fail condition for this check
+ """
+ min_mem = default_resource_map.get('min_memory')
+ total_memory = round(psutil.virtual_memory().total / (1024**3))
+ min_memory = round(human_memory_to_bytes(min_mem) / (1024**3))
+
+ if total_memory < min_memory:
+ raise ConfigError(
+ 'This system does not meet minimal requirements for VPP. '
+ f'Minimum {min_memory} GB of RAM are required.'
+ )
+
+
+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')
+
+ readable_heap_page = bytes_to_human_memory(main_heap_page_size, 'K')
+
+ if main_heap_page_size > main_heap_size:
+ raise ConfigError(
+ f'The main heap size must be greater than or equal to page-size ({readable_heap_page})'
+ )
+
+
+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']
+ )
+
+ # 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'
+ )
+
+ if errors:
+ raise ConfigError(
+ '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 ''
+ )
+ )
+
+
+def verify_vpp_settings_cpu_skip_cores(skip_cores: int):
+ cpu_cores = total_core_count()
+
+ # The number of skipped cores must not be greater than
+ # available CPU cores in the system - 1 for main thread
+ if skip_cores > (cpu_cores - 1):
+ raise ConfigError(
+ f'The system does not have enough available CPUs to skip '
+ f'(reduce "cpu skip-cores" to {cpu_cores} or less)'
+ )
+
+
+def verify_vpp_settings_cpu_and_corelist_workers(settings: dict):
+ """
+ `set vpp settings cpu workers` and `set vpp settings cpu corelist-workers`
+ are mutually exclusive!
+ """
+ if (
+ 'corelist_workers' in settings or 'workers' in settings
+ ) and 'main_core' not in settings:
+ raise ConfigError('"cpu main-core" is required but not set!')
+
+ if 'corelist_workers' in settings and 'workers' in settings:
+ raise ConfigError(
+ '"cpu corelist-workers" and "cpu workers" cannot be used at the same time!'
+ )
+
+
+def verify_vpp_cpu_main_core(cpu_settings: dict) -> None:
+ """Check that the main core is available"""
+ skip_cores = int(cpu_settings.get('skip_cores', 0))
+ available_cores = cpu_checks.available_cores_list(skip_cores)
+ main_core = int(cpu_settings['main_core'])
+
+ if main_core not in available_cores:
+ raise ConfigError(
+ 'Cannot set main core for VPP process: '
+ f'CPU#{main_core} is not available.'
+ )
+
+
+def verify_vpp_settings_cpu_workers(cpu_settings: dict) -> int:
+ """
+ Verify that the system has enough available CPU cores
+ to run a given amount of worker processes (1 worker/core)
+ """
+ workers = int(cpu_settings.get('workers', 0))
+ available_cores = cpu_checks.available_cores_count(cpu_settings)
+
+ if workers > available_cores:
+ raise ConfigError(
+ f'Not enough free CPU cores for {workers} VPP workers '
+ f'(reduce to {available_cores} or less)'
+ )
+
+ return workers
+
+
+def verify_vpp_settings_cpu_corelist_workers(cpu_settings: dict) -> int:
+ """
+ Verify that the CPU cores provided to the config are free and can be used by VPP
+ """
+ workers = cpu_settings.get('corelist_workers')
+ main_core = int(cpu_settings.get('main_core'))
+ skip_cores = int(cpu_settings.get('skip_cores', 0))
+ available_cores = cpu_checks.available_cores_list(skip_cores)
+ try:
+ all_core_nums = cpu_checks.worker_cores_list(
+ iface='cpu corelist', worker_ranges=workers
+ )
+ except ValueError as e:
+ raise ConfigError(str(e))
+
+ error_msg = 'Cannot set VPP "cpu corelist-workers"'
+
+ if main_core in all_core_nums:
+ raise ConfigError(
+ f'CPU#{main_core} is set as main core and should not '
+ 'be included to the corelist-workers'
+ )
+
+ invalid_cores = [str(el) for el in all_core_nums if el not in available_cores]
+ if invalid_cores:
+ raise ConfigError(
+ f'{error_msg}: CPU# {",".join(invalid_cores)} are not available.'
+ )
+
+ 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:
+ raise ConfigError(
+ '"nat44 workers" requires cpu workers or corelist-workers to be set!'
+ )
+ try:
+ nat_workers = cpu_checks.worker_cores_list(
+ iface='nat44', worker_ranges=nat44_workers
+ )
+ except ValueError as e:
+ raise ConfigError(str(e))
+
+ invalid_workers = [str(el) for el in nat_workers if el not in range(workers)]
+ if invalid_workers:
+ raise ConfigError(
+ f'Cannot set VPP "nat44 workers": worker(s) #{",".join(invalid_workers)} not available. '
+ f'Available worker ids: {",".join(map(str, range(workers)))}'
+ )
+
+
+def verify_vpp_statseg_size(settings: dict):
+ statseg_size = mem_checks.statseg_size(settings)
+
+ if 'size' in settings.get('statseg'):
+ if statseg_size < 128 << 20:
+ raise ConfigError('The statseg size must be greater than or equal to 128M')
+
+ if 'page_size' in settings['statseg']:
+ statseg_page_size = mem_checks.statseg_page_size(settings)
+ if statseg_page_size > statseg_size:
+ readable_statseg_page = bytes_to_human_memory(statseg_page_size, 'K')
+ raise ConfigError(
+ f'The statseg size must be greater than or equal to page-size ({readable_statseg_page})'
+ )
+
+
+def verify_vpp_interfaces_dpdk_num_queues(qtype: str, num_queues: int, workers: int):
+ """
+ Verify that VPP has enough workers to run the given amount of RX/TX queues
+ 1 queue per 1 worker is assumed as default
+ """
+
+ if num_queues > workers:
+ raise ConfigError(
+ f'The number of {qtype} queues cannot be greater than the number of configured VPP workers: '
+ f'workers: {workers}, queues: {num_queues}'
+ )
+
+
+def verify_routes_count(settings: dict, workers: int):
+ """
+ Maximum routes count depending on main heap size,
+ statistics segment size and workers
+ """
+ counters = 2 # 2 counters for each route
+ bytes = 16 # each counter consumes 16 bytes
+ statseg_scale = 2
+ statseg_size = settings['statseg']['size']
+ statseg_size_in_bytes = human_memory_to_bytes(statseg_size)
+ main_heap = settings['memory']['main_heap_size']
+ main_heap_in_gb = human_memory_to_bytes(main_heap) >> 30
+
+ formula = (workers + 1) * counters * bytes * statseg_scale
+ routes_count_statseg = statseg_size_in_bytes / formula
+ routes_count_statseg = round(routes_count_statseg / 1_000_000, 2)
+ routes_count_mh = main_heap_in_gb * 2
+ routes_count_min = min(routes_count_statseg, routes_count_mh)
+ Warning(
+ f'NOTE: Current dataplane capacity (estimated): {routes_count_min} M IPv4 routes. '
+ 'Exceeding these values will lead to a dataplane out-of-memory condition and a crash. '
+ 'Extensive use of features like ACLs, NAT and others may reduce the numbers above. '
+ 'Please read the documentation for details: https://docs.vyos.io/'
+ )
diff --git a/python/vyos/vpp/configdb.py b/python/vyos/vpp/configdb.py
new file mode 100644
index 000000000..13c1cd3a0
--- /dev/null
+++ b/python/vyos/vpp/configdb.py
@@ -0,0 +1,206 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from json import loads as json_loads, dumps as json_dumps
+from pathlib import Path
+from typing import Any
+
+STORAGE_LOCATION = '/run/vpp'
+
+
+class JSONStorage:
+ def __init__(self, name: str = '') -> None:
+ """Initiate a file storage
+
+ Args:
+ name (str, optional): Unique storage name. Defaults to '' (generate a name).
+
+ Raises:
+ err: In case a file for storage cannot be created
+ """
+ # If a name is not provided, this is a temporary one-time storage
+ # this use case is strange, but let's allow this
+ if not name:
+ self.__temporary = True
+ self.__cache: dict[Any, Any] = {}
+ self.__locked = False
+ return
+ self.__temporary = False
+
+ self.__storage = Path(f'{STORAGE_LOCATION}/{name}.json')
+ self.__lock_file = Path(f'{STORAGE_LOCATION}/{name}.lock')
+
+ # prepare a folder
+ storage_dir = Path(STORAGE_LOCATION)
+ if not storage_dir.exists():
+ storage_dir.mkdir(parents=True)
+
+ # initialize lock status
+ self.__locked = False
+ if self.__storage_locked():
+ raise FileExistsError(f'Cannot open locked storage: {self.__storage}')
+ self.__lock_file.touch()
+
+ if not self.__storage.exists():
+ try:
+ self.__storage.touch()
+ except Exception as err:
+ print(f'Unable to initiate storage: {err}')
+ raise err
+ # prepare an empty cache
+ self.__cache: dict[Any, Any] = {}
+ else:
+ # load a cache from file
+ self.__cache = self.__load_file()
+
+ def __del__(self) -> None:
+ """Dump data to persistent storage and unlock it"""
+ if self.__temporary:
+ return
+ # dump a cache to storage
+ if self.__cache:
+ self.__dump_file()
+ # or remove a file
+ else:
+ self.__storage.unlink()
+ # unlock a storage
+ self.__lock_file.unlink()
+
+ def __check_types(self, data: Any) -> None:
+ """Check if all the data have supported types
+
+ Args:
+ data (Any): object to validate
+
+ Raises:
+ TypeError: If a data type is not supported
+ """
+ if isinstance(data, str | int | float | bool | None):
+ return
+ if isinstance(data, list):
+ for item in data:
+ self.__check_types(item)
+ return
+ if isinstance(data, dict):
+ for item in data.values():
+ self.__check_types(item)
+ return
+ raise TypeError(f'Object type "{type(data)}" is not allowed')
+
+ def __load_file(self) -> dict[Any, Any]:
+ """Read a file to a dictionary
+
+ Returns:
+ dict[Any, Any]: loaded dict object
+ """
+ data: bytes = self.__storage.read_bytes()
+ return json_loads(data)
+
+ def __dump_file(self) -> None:
+ """Dump cache to a file"""
+ data: str = json_dumps(self.__cache)
+ self.__storage.write_text(data)
+
+ def __lock(self) -> None:
+ """Lock storage
+
+ Raises:
+ FileExistsError: Raised if a storage is already locked
+ """
+ if self.__locked:
+ raise FileExistsError(f'Access is already locked: {self.__storage}')
+ self.__locked = True
+
+ def __unlock(self) -> None:
+ """Unlock storage
+
+ Raises:
+ FileNotFoundError: Raised if a storage is already unlocked
+ """
+ if not self.__locked:
+ raise FileNotFoundError(f'Access is already unlocked: {self.__storage}')
+ self.__locked = False
+
+ def __storage_locked(self) -> bool:
+ """Check if a storage is locked
+
+ Returns:
+ bool: Lock status
+ """
+ if self.__lock_file.exists():
+ return True
+ return False
+
+ def delete(self, key: Any = None) -> None:
+ """Delete data from a storage or a full storage
+
+ Raises:
+ FileExistsError: Raised if a storage is locked
+ """
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage locked and delete operation cannot be performed: {self.__storage}'
+ )
+ if key:
+ if key not in self.__cache:
+ raise ValueError(
+ f'Object {key} does not exist in storage {self.__storage}'
+ )
+ del self.__cache[key]
+ else:
+ self.__cache = {}
+
+ def write(self, key: Any, value: Any) -> None:
+ # Check types first
+ self.__check_types(key)
+ self.__check_types(value)
+ # check lock status
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage is locked and cannot be written: {self.__storage}'
+ )
+ # write a data to a cache
+ self.__lock()
+ self.__cache[key] = value
+ self.__unlock()
+
+ def read(self, key: Any, default: Any = None) -> Any:
+ """Read data from a storage
+
+ Args:
+ key (Any): key name
+ default (Any, optional): Value to return if a key does not exist. Defaults to None.
+
+ Raises:
+ FileExistsError: Raised if a storage is locked
+
+ Returns:
+ Any: Value to return
+ """
+ # Check types first
+ self.__check_types(key)
+ # check lock status
+ if self.__locked:
+ raise FileExistsError(
+ f'Storage is locked and it is not safe to read: {self.__storage}'
+ )
+ # read a data from cache
+ self.__lock()
+ data: Any | None = self.__cache.get(key, default)
+ self.__unlock()
+
+ return data
diff --git a/python/vyos/vpp/control_host.py b/python/vyos/vpp/control_host.py
new file mode 100644
index 000000000..6a6b9d72f
--- /dev/null
+++ b/python/vyos/vpp/control_host.py
@@ -0,0 +1,375 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
+from pathlib import Path
+from re import fullmatch as re_fullmatch
+from subprocess import run
+from time import sleep
+
+from pyroute2 import IPRoute
+
+from vyos.vpp.utils import EthtoolGDrvinfo
+
+
+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 ''.
+ """
+ device_file = Path(f'/sys/bus/pci/devices/{pci_addr}/remove')
+ if pci_addr:
+ 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}'
+ )
+
+
+def unbind_driver(bus_id: str, device_id: str) -> bool:
+ """Unbind a driver from a device
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+
+ Returns:
+ bool: True if a driver has been unbound, False otherwise
+ """
+ device_resolved: str = (
+ Path(f'/sys/bus/{bus_id}/devices/{device_id}').resolve().as_posix()
+ )
+ if not Path(f'{device_resolved}/driver').exists():
+ return False
+
+ Path(f'{device_resolved}/driver/unbind').write_text(device_id)
+ return True
+
+
+def probe_driver(bus_id: str, device_id: str) -> None:
+ """Probe driver for a device on a bus
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+ """
+ Path(f'/sys/bus/{bus_id}/drivers_probe').write_text(device_id)
+
+
+def load_kernel_module(module_name: str) -> None:
+ """Load a kernel module
+
+ Args:
+ module_name (str): module name
+ """
+ # check if a module already loaded
+ if Path(f'/sys/module/{module_name}').exists():
+ return
+
+ # execute modprobe with the specified module name
+ run(['/usr/sbin/modprobe', '-q', module_name], check=True)
+
+
+def override_driver(bus_id: str, device_id: str, driver_name: str = '') -> None:
+ """Override a driver for a device
+
+ Args:
+ bus_id (str): bus ID (pci, vmbus, etc.)
+ device_id (str): device id on the bus (PCI address, VMBus UUID)
+ driver_name (str, optional): Kernel module (driver) name. Defaults to '' - clear an override.
+
+ Raises:
+ FileNotFoundError: A device does not support driver override
+ ChildProcessError: Failed to override a driver
+ """
+ device_resolved: str = (
+ Path(f'/sys/bus/{bus_id}/devices/{device_id}').resolve().as_posix()
+ )
+ # check if a device supports driver override
+ if not Path(f'{device_resolved}/driver_override').exists():
+ raise FileNotFoundError(f'{device_resolved} does not support driver override')
+
+ if driver_name:
+ load_kernel_module(driver_name)
+
+ unbind_driver(bus_id, device_id)
+
+ # vfio-pci needs special approach
+ if driver_name == 'vfio-pci':
+ vendor: str = Path(f'{device_resolved}/vendor').read_text()
+ device: str = Path(f'{device_resolved}/device').read_text()
+ Path('/sys/module/vfio_pci/drivers/pci:vfio-pci/new_id').write_text(
+ f'{vendor} {device}'
+ )
+
+ # override a driver
+ Path(f'{device_resolved}/driver_override').write_text(f'{driver_name}\n')
+
+ # probe a driver
+ probe_driver(bus_id, device_id)
+
+ # check the result
+ if not Path(f'{device_resolved}/driver').exists():
+ raise ChildProcessError(
+ f'Failed to override a driver to {driver_name} for {bus_id}, {device_id}'
+ )
+
+
+def get_bus_name(iface: str) -> str:
+ """Get bus name
+ Works for PCI, VMbus, maybe something else.
+ Does not work for Virtio and other virtual devices
+ (however, it does not seem we need this for such kind of devices).
+
+ Args:
+ iface (str): interface name
+
+ Returns:
+ str: bus name
+ """
+ device_resolved: Path = Path(f'/sys/class/net/{iface}/device').resolve()
+
+ # Iterate upwards until a `bus` directory is found
+ current_path: Path = device_resolved
+ while True:
+ # Check if a bus info is available
+ subsystem_path = Path(f'{current_path}/subsystem')
+ if subsystem_path.is_symlink():
+ # Read the link to determine the bus type
+ bus_path = subsystem_path.resolve()
+ # Check if the parent directory is a 'bus' directory in '/sys/bus/'
+ if bus_path.parent.name == 'bus':
+ # Return only the last name of the path, e.g., 'pci'
+ return bus_path.name
+
+ # Move up one directory level
+ current_path = current_path.parent
+ if current_path == Path('/sys'):
+ break # Stop if we reach the root of /sys without finding a bus type
+
+ return '' # Return None if no bus type was found
+
+
+def get_eth_name(dev_id: str) -> str:
+ """Find Ethernet interface name by PCI address or UUID
+
+ Args:
+ dev_id (str): PCI address or UUID
+
+ 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():
+ # PCI devices
+ 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})
+ # UUID devices
+ else:
+ try:
+ bus_type: str = get_bus_name(dir.name)
+ iface_addr = EthtoolGDrvinfo(dir.name).bus_info_expand(bus_type)
+ net_devs.update({iface_addr: dir.name})
+ except FileNotFoundError:
+ pass
+
+ # match to provided PCI address or UUID and return a name if found
+ if dev_id in net_devs:
+ return net_devs[dev_id]
+ # raise error if device was not found
+ raise FileNotFoundError(
+ f'A device with ID {dev_id} not found in ethernet interfaces'
+ )
+
+
+def get_dev_id(iface: str) -> str:
+ """Get device ID by its interface name
+
+ Args:
+ iface (str): interface name
+
+ Raises:
+ FileNotFoundError: no Ethernet interface was found
+
+ Returns:
+ str: device ID (PCI address or UUID)
+ """
+ try:
+ # Try to get details via ethtool first
+ ethtool_info = EthtoolGDrvinfo(iface)
+ # For devices represented by UUID we need to expand them
+ # to their full representation
+ if ethtool_info.driver == 'hv_netvsc':
+ return ethtool_info.bus_info_expand('vmbus')
+ return ethtool_info.bus_info
+ except Exception:
+ # raise error if a device ID was not found
+ raise FileNotFoundError(f'Cannot find device ID for interface {iface}')
+
+
+def get_eth_driver(iface: str) -> str:
+ """Find kernel module used for Ethernet interface
+
+ Args:
+ iface (str): Ethernet interface name
+
+ Raises:
+ FileNotFoundError: no Ethernet interface was found
+
+ Returns:
+ str: kernel module name
+ """
+ iface_driver: str = ''
+ driver_dir = Path(f'/sys/class/net/{iface}/device/driver/module')
+ if not driver_dir.exists():
+ # raise error if device was not found
+ raise FileNotFoundError(f'PCI device {iface} not found in ethernet interfaces')
+
+ iface_driver: str = driver_dir.resolve().name
+ return iface_driver
+
+
+def unsafe_noiommu_mode(status: bool) -> None:
+ """Control unsafe_noiommu_mode parameter of vfio module
+
+ Args:
+ status (bool): Target status
+
+ Raises:
+ ChildProcessError: Raised if failed to set unsafe_noiommu_mode
+ """
+ param_path = Path('/sys/module/vfio/parameters/enable_unsafe_noiommu_mode')
+ current_status: str = param_path.read_text().strip()
+ target_status: str = 'Y' if status else 'N'
+ if current_status != target_status:
+ param_path.write_text(target_status)
+ if param_path.read_text().strip() != target_status:
+ raise ChildProcessError('Failed to set unsafe_noiommu_mode')
+
+
+def rename_iface(name_old: str, name_new: str) -> None:
+ """Rename interface
+
+ Args:
+ name_old (str): old name
+ name_new (str): new name
+ """
+ run(['ip', 'link', 'set', name_old, 'down'])
+ rename_cmd: list[str] = ['ip', 'link', 'set', name_old, 'name', name_new]
+ run(rename_cmd)
+
+
+def set_promisc(iface_name: str, operation: str) -> None:
+ """Set promisc mode for interface
+
+ Args:
+ iface_name (str): name of an interface
+ operation (str): operation (on, off)
+ """
+ run(['ip', 'link', 'set', iface_name, 'promisc', operation])
+
+
+def set_mtu(iface_name: str, mtu: int) -> None:
+ """Set MTU for interface
+
+ Args:
+ iface_name (str): name of an interface
+ mtu (int): MTU
+ """
+ run(['ip', 'link', 'set', iface_name, 'mtu', str(mtu)])
+
+
+def get_eth_mac(iface_name: str) -> str:
+ """Get MAC address of an interface
+
+ Args:
+ iface_name (str): name of an interface
+
+ Raises:
+ FileNotFoundError: interface was not found
+
+ Returns:
+ str: MAC address
+ """
+ dev_addr_path = Path(f'/sys/class/net/{iface_name}/address')
+ if dev_addr_path.exists():
+ return dev_addr_path.read_text().strip()
+ else:
+ # raise error if device was not found
+ raise FileNotFoundError(f'Interface {iface_name} not found')
+
+
+def xdp_remove(iface_name: str) -> None:
+ """Remove XDP BPF program from an interfce
+
+ Args:
+ iface_name (str): name of an interface
+ """
+ run(['ip', 'link', 'set', iface_name, 'xdp', 'off'])
+
+
+def set_status(iface_name: str, status: str) -> None:
+ """Set interface status
+
+ Args:
+ iface_name (str): name of an interface
+ status (str): status - "up" or "down"
+ """
+ run(['ip', 'link', 'set', iface_name, status])
+
+
+def flush_ip(iface_name: str) -> None:
+ """Flush IP addresses from an interface
+
+ Args:
+ iface_name (str): name of an interface
+ """
+ iproute = IPRoute()
+ iproute.flush_addr(label=iface_name)
diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py
new file mode 100644
index 000000000..596ef4f12
--- /dev/null
+++ b/python/vyos/vpp/control_vpp.py
@@ -0,0 +1,488 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from collections.abc import Callable
+from functools import wraps
+from re import search as re_search, MULTILINE as re_M
+from systemd import journal
+from time import sleep
+from typing import TypeVar, ParamSpec, Literal
+
+from vpp_papi import VPPApiClient
+from vpp_papi import VPPIOError, VPPValueError
+
+# define types for static type checkers
+AnyType = TypeVar('AnyType')
+AnyParam = ParamSpec('AnyParam')
+
+
+class VPPControl:
+ """Control VPP network stack"""
+
+ class _Decorators:
+ """Decorators for VPPControl"""
+
+ @classmethod
+ def api_call(
+ cls, decorated_func: Callable[AnyParam, AnyType]
+ ) -> Callable[AnyParam, AnyType]:
+ """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: AnyParam.args, **kwargs: AnyParam.kwargs
+ ) -> AnyType:
+ if not cls.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: Callable[AnyParam, AnyType]
+ ) -> Callable[AnyParam, AnyType]:
+ """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: AnyParam.args, **kwargs: AnyParam.kwargs
+ ) -> AnyType:
+ 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:
+ error_message = f'VPP API connection timeout: {err}'
+ journal.send(error_message, priority=journal.LOG_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.api_call
+ def get_interface_name(self, index: int) -> str | None:
+ """Find interface name by interface index in VPP
+
+ Args:
+ index (int): interface index inside VPP
+
+ Returns:
+ str | None: Interface name or None (if was not found)
+ """
+ for iface in self.__vpp_api_client.api.sw_interface_dump():
+ if iface.sw_if_index == index:
+ return iface.interface_name
+ return None
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def lcp_pair_add(
+ self,
+ iface_name_vpp: str,
+ iface_name_kernel: str,
+ iface_type: Literal['tun', 'tap', ''] = '',
+ ) -> 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_type (Literal['tun', 'tap', ''], optional): Use explicit interface type in kernel. Defaults to ''.
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ api_call_args: dict[str, bool | int | str] = {
+ 'is_add': True,
+ 'sw_if_index': iface_index,
+ 'host_if_name': iface_name_kernel,
+ }
+ if iface_type:
+ iface_type_resolve = {'tun': 1, 'tap': 0}
+ api_call_args['host_if_type'] = iface_type_resolve[iface_type]
+ return self.__vpp_api_client.api.lcp_itf_pair_add_del_v2(**api_call_args)
+
+ @_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_v2(
+ is_add=False, sw_if_index=iface_index, host_if_name=iface_name_kernel
+ )
+
+ @_Decorators.api_call
+ def lcp_pair_find(
+ self,
+ kernel_name: str = '',
+ vpp_index_hw: int | None = None,
+ vpp_index_kernel: int | None = None,
+ vpp_name_hw: str = '',
+ vpp_name_kernel: str = '',
+ ) -> dict[str, str | int] | None:
+ """Find LCP pair details
+
+ Args:
+ kernel_name (str, optional): Interface name in the kernel. Defaults to ''.
+ vpp_index_hw (int | None, optional): Interface index in VPP (hardware). Defaults to None.
+ vpp_index_kernel (int | None, optional): Interface index in VPP (kernel). Defaults to None.
+ vpp_name_hw (str, optional): Interface name in VPP (hardware). Defaults to ''.
+ vpp_name_kernel (str, optional): Interface name in VPP (to kernel). Defaults to ''.
+
+ Returns:
+ dict[str, str | int] | None: LCP pair details
+ """
+ filter_dict = {}
+ for filter_name, filter_value in locals().items():
+ if filter_value:
+ filter_dict[filter_name] = filter_value
+
+ # Get list of pairs
+ lcp_pairs = self.lcp_pairs_list()
+
+ # Check each pair
+ for pair in lcp_pairs:
+ pair_found = False
+ # For each item provided in function arguments
+ for filter_name, filter_value in filter_dict.items():
+ # Stop if filter value is not as in a current pair
+ if filter_name in pair and pair[filter_name] != filter_value:
+ pair_found = False
+ break
+ # Set flag to True and check the next filter value
+ pair_found = True
+
+ if pair_found:
+ return pair
+
+ return None
+
+ @_Decorators.api_call
+ def lcp_pairs_list(self) -> list[dict[str, str | int]]:
+ """List all LCP pairs
+
+ Returns:
+ list[dict[str, str | int]]: LCP pairs details
+ """
+ lcp_pairs_details = []
+
+ lcp_pairs = self.__vpp_api_client.api.lcp_itf_pair_get()[1]
+ vpp_ifaces = self.__vpp_api_client.api.sw_interface_dump()
+ for pair in lcp_pairs:
+ pair_details = {
+ 'kernel_name': pair.host_if_name,
+ 'vpp_index_hw': pair.phy_sw_if_index,
+ 'vpp_index_kernel': pair.host_sw_if_index,
+ }
+ for vpp_iface in vpp_ifaces:
+ if vpp_iface.sw_if_index == pair_details['vpp_index_hw']:
+ pair_details['vpp_name_hw'] = vpp_iface.interface_name
+ if vpp_iface.sw_if_index == pair_details['vpp_index_kernel']:
+ pair_details['vpp_name_kernel'] = vpp_iface.interface_name
+
+ lcp_pairs_details.append(pair_details)
+
+ return lcp_pairs_details
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def lcp_resync(self) -> None:
+ """Resynchronize objects between kernel and VPP via Netlink
+
+ This clears all routes in VPP configured by LCP and re-creates them
+ based on the current state of the kernel.
+ """
+ return self.__vpp_api_client.api.lcp_nl_resync()
+
+ @_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 match 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
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def xdp_iface_create(
+ self,
+ host_if: str,
+ name: str,
+ rxq_num: int = 0,
+ rxq_size: int = 0,
+ txq_size: int = 0,
+ mode: Literal['auto', 'copy', 'zero-copy'] = 'auto',
+ flags: Literal['no_syscall_lock', ''] = '',
+ ) -> None:
+ """Create XDP interface
+
+ Args:
+ host_if (str): name of an interface in kernel
+ name (str): name of an interface in VPP
+ rxq_num (int, optional): Number of receive queues to connect to. Defaults to 0 (all).
+ rxq_size (int, optional): Size of receive queue. Defaults to 0.
+ txq_size (int, optional): Size of tranceive queue. Defaults to 0.
+ mode (Literal['auto', 'copy', 'zero-copy', optional): Zero-copy mode. Defaults to 'auto'.
+ flags (Literal['no_syscall_lock', ''], optional): Syscall lock mode. Defaults to ''.
+ """
+ api_call_args: dict[str, int | str] = {
+ 'host_if': host_if,
+ 'name': name,
+ 'rxq_num': rxq_num,
+ 'rxq_size': rxq_size,
+ 'txq_size': txq_size,
+ }
+ if mode != 'auto':
+ mode_resolve: dict[str, int] = {'auto': 0, 'copy': 1, 'zero-copy': 2}
+ api_call_args['mode'] = mode_resolve[mode]
+ if flags == 'no_systcall_lock':
+ api_call_args['flags'] = 1
+ return self.__vpp_api_client.api.af_xdp_create_v3(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def xdp_iface_delete(self, iface_name_vpp: str) -> None:
+ """Delete XDP interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ if iface_index:
+ api_call_args: dict[str, int] = {'sw_if_index': iface_index}
+ return self.__vpp_api_client.api.af_xdp_delete(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def set_iface_mac(self, iface_name_vpp: str, mac_address: str) -> None:
+ """Set MAC address of an interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ mac_address (str): MAC address
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ api_call_args: dict[str, str | int] = {
+ 'sw_if_index': iface_index,
+ 'mac_address': mac_address,
+ }
+ return self.__vpp_api_client.api.sw_interface_set_mac_address(**api_call_args)
+
+ @_Decorators.check_retval
+ @_Decorators.api_call
+ def set_iface_mtu(self, iface_name_vpp: str, mtu: int) -> None:
+ """Set MTU for interface
+
+ Args:
+ iface_name_vpp (str): Name of an interface in VPP
+ mtu (int): MTU
+ """
+ iface_index = self.get_sw_if_index(iface_name_vpp)
+ api_call_args: dict[str, str | int] = {'sw_if_index': iface_index, 'mtu': mtu}
+ return self.__vpp_api_client.api.hw_interface_set_mtu(**api_call_args)
+
+ @_Decorators.api_call
+ def get_sw_if_dev_type(self, ifname: str) -> int | None:
+ """Find interface device type by interface name in VPP
+
+ Args:
+ ifname (str): interface name inside VPP
+
+ Returns:
+ int | None: Interface device type or None (if was not fount)
+ """
+ for iface in self.__vpp_api_client.api.sw_interface_dump():
+ if iface.interface_name == ifname:
+ return iface.interface_dev_type
+ return None
+
+ @_Decorators.api_call
+ def enable_disable_nat44_forwarding(self, enable: bool) -> None:
+ """Enable/disable NAT44 forwarding
+
+ Args:
+ enable (bool): True if enable, False if disable
+ """
+ self.__vpp_api_client.api.nat44_forwarding_enable_disable(enable=enable)
+
+ @_Decorators.api_call
+ def set_nat44_session_limit(self, session_limit: int) -> None:
+ """Set NAT44 session limit
+
+ Args:
+ session_limit (int): Maximum number of sessions per thread
+ """
+ self.__vpp_api_client.api.nat44_set_session_limit(
+ session_limit=session_limit,
+ )
+
+ @_Decorators.api_call
+ def set_nat_workers(self, workers: int) -> None:
+ """Set NAT44 session limit
+
+ Args:
+ workers (int): Bitmask of workers list
+ """
+ self.__vpp_api_client.api.nat_set_workers(worker_mask=workers)
+
+ @property
+ def connected(self) -> bool:
+ """Check if VPP API is connected
+
+ Returns:
+ bool: True if connected, False if not
+ """
+ return self.__vpp_api_client.transport.connected
+
+ @property
+ @_Decorators.api_call
+ def api(self):
+ """Call API
+
+ Returns:
+ Callable[AnyParam, AnyType]: API functions
+ """
+ return self.__vpp_api_client.api
diff --git a/python/vyos/vpp/interface/__init__.py b/python/vyos/vpp/interface/__init__.py
new file mode 100644
index 000000000..903b7cbfc
--- /dev/null
+++ b/python/vyos/vpp/interface/__init__.py
@@ -0,0 +1,42 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from .bond import BondInterface
+from .bridge import BridgeInterface
+from .ethernet import EthernetInterface
+from .geneve import GeneveInterface
+from .gre import GREInterface
+from .interface import Interface
+from .ipip import IPIPInterface
+from .loopback import LoopbackInterface
+from .vxlan import VXLANInterface
+from .wireguard import WireguardInterface
+from .xconnect import XconnectInterface
+
+__all__ = [
+ 'BondInterface',
+ 'BridgeInterface',
+ 'EthernetInterface',
+ 'GeneveInterface',
+ 'GREInterface',
+ 'Interface',
+ 'IPIPInterface',
+ 'LoopbackInterface',
+ 'VXLANInterface',
+ 'WireguardInterface',
+ 'XconnectInterface',
+]
diff --git a/python/vyos/vpp/interface/bond.py b/python/vyos/vpp/interface/bond.py
new file mode 100644
index 000000000..604ba2ae5
--- /dev/null
+++ b/python/vyos/vpp/interface/bond.py
@@ -0,0 +1,118 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp.control_host import set_promisc
+from vyos.vpp.interface.interface import Interface
+
+
+class BondInterface(Interface):
+ def __init__(
+ self,
+ ifname,
+ mode: str = '',
+ load_balance: int = 0,
+ mac: str = '',
+ kernel_interface: str = '',
+ state: str = 'up',
+ ):
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('bond'))
+ self.ifname = f'BondEthernet{self.instance}'
+ self.mode = mode
+ self.load_balance = load_balance
+ self.mac = mac
+ self.kernel_interface = kernel_interface
+ self.state = state
+
+ def add(self):
+ """Create Bond interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/bonding/bond.api
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.add()
+ """
+ # Create interface 'bondX'
+ create_args = {
+ 'id': self.instance,
+ 'mode': self.mode,
+ 'lb': self.load_balance,
+ }
+ if self.mac:
+ create_args.update({'use_custom_mac': True, 'mac_address': self.mac})
+ self.vpp.api.bond_create2(**create_args)
+ if self.kernel_interface:
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+ # Set interface state
+ self.set_state(self.state)
+
+ def delete(self):
+ """Delete Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.delete()
+ """
+ bond_if_index = self.vpp.get_sw_if_index(self.ifname)
+ self.vpp.api.bond_delete(sw_if_index=bond_if_index)
+
+ def add_member(self, interface):
+ """Add member to Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.add_member(interface='eth0')
+ """
+ bond_if_index = self.vpp.get_sw_if_index(f'BondEthernet{self.instance}')
+ member_if_index = self.vpp.get_sw_if_index(interface)
+ member_if_type = self.vpp.get_sw_if_dev_type(interface)
+ self.vpp.api.bond_add_member(
+ bond_sw_if_index=bond_if_index, sw_if_index=member_if_index
+ )
+ self.vpp.api.sw_interface_set_promisc(
+ sw_if_index=member_if_index, promisc_on=True
+ )
+ if member_if_type == 'AF_XDP interface':
+ set_promisc(f'defunct_{interface}', 'on')
+
+ def detach_member(self, interface):
+ """Detach member from Bond interface
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0')
+ a.detach_member(interface='eth0')
+ """
+ member_if_index = self.vpp.get_sw_if_index(interface)
+ self.vpp.api.bond_detach_member(sw_if_index=member_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import BondInterface
+ a = BondInterface(ifname='bond0', mode=5)
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/bridge.py b/python/vyos/vpp/interface/bridge.py
new file mode 100644
index 000000000..25f0b4660
--- /dev/null
+++ b/python/vyos/vpp/interface/bridge.py
@@ -0,0 +1,128 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+
+class BridgeInterface:
+ def __init__(
+ self,
+ ifname: str,
+ flood: bool = True,
+ forward: bool = True,
+ learn: bool = True,
+ uu_flood: bool = True,
+ arp_term: bool = False,
+ ):
+ self.ifname = ifname
+ self.interface_suffix = int(self.ifname.replace('br', ''))
+ self.flood = flood
+ self.forward = forward
+ self.learn = learn
+ self.uu_flood = uu_flood
+ self.arp_term = arp_term
+ self.vpp = VPPControl()
+
+ def add(self):
+ """Create Bridge interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/l2/l2.api
+
+ Bridge-domain 0 is reserved for the default bridge-domain.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.add()
+ """
+ self.vpp.api.bridge_domain_add_del_v2(
+ is_add=True,
+ bd_id=self.interface_suffix,
+ flood=self.flood,
+ forward=self.forward,
+ learn=self.learn,
+ uu_flood=self.uu_flood,
+ arp_term=self.arp_term,
+ )
+
+ def delete(self):
+ """Delete Bridge interface
+
+ Bridge-members must be detached before deleting the bridge interface.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.delete()
+ """
+ self.vpp.api.bridge_domain_add_del_v2(is_add=False, bd_id=self.interface_suffix)
+
+ def add_member(self, member: str | int, port_type: int = 0):
+ """Add member to Bridge interface
+
+ Attaches a VPP interface to the Bridge interface specified by `interface_suffix`.
+ The `member` parameter can be either the name (str) or the index (int) of the network
+ VPP interface to be added as a member to the bridge.
+
+ Args:
+ member (str or int): The name or index of the VPP network interface
+ to be added as a member to the bridge.
+ port_type: 0 - Normal port, 1 - BVI port
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.add_member(member='eth0')
+ """
+ bridge_index = self.interface_suffix
+ # If the 'member' is an Integer or digit, assume it's an interface index
+ if isinstance(member, int):
+ member_if_index = member
+ elif member.isdigit():
+ member_if_index = int(member)
+ else:
+ member_if_index = self.vpp.get_sw_if_index(member)
+
+ return self.vpp.api.sw_interface_set_l2_bridge(
+ rx_sw_if_index=member_if_index, bd_id=bridge_index, port_type=port_type
+ )
+
+ def detach_member(self, member: str | int):
+ """Detach member from Bridge interface.
+ Bridge-domain 0 is reserved for the default bridge-domain.
+ The `member` parameter can be either the name (str) or the index (int)
+ of the network VPP interface
+
+ Args:
+ member (str or int): The name or index of the VPP network interface
+ to be detached from the bridge.
+
+ Example:
+ from vyos.vpp.interface import BridgeInterface
+ a = BridgeInterface(ifname='br23')
+ a.detach_member(member='eth0')
+ """
+ # If the 'member' is an Integer or digit, assume it's an interface index
+ if isinstance(member, int):
+ member_if_index = member
+ elif member.isdigit():
+ member_if_index = int(member)
+ else:
+ member_if_index = self.vpp.get_sw_if_index(member)
+
+ return self.vpp.api.sw_interface_set_l2_bridge(
+ rx_sw_if_index=member_if_index, bd_id=0, port_type=0
+ )
diff --git a/python/vyos/vpp/interface/ethernet.py b/python/vyos/vpp/interface/ethernet.py
new file mode 100644
index 000000000..523211db8
--- /dev/null
+++ b/python/vyos/vpp/interface/ethernet.py
@@ -0,0 +1,53 @@
+# VyOS implementation of VPP Ethernet interface
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+
+class EthernetInterface:
+ """Interface Ethernet"""
+
+ def __init__(self, ifname, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('eth'))
+ self.ifname = ifname
+ self.kernel_interface = kernel_interface
+ self.vpp = VPPControl()
+
+ def add(self):
+ pass
+
+ def delete(self):
+ pass
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import EthernetInterface
+ a = EthernetInterface(ifname='eth0')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import EthernetInterface
+ a = EthernetInterface(ifname='eth0')
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/geneve.py b/python/vyos/vpp/interface/geneve.py
new file mode 100644
index 000000000..2d6347f12
--- /dev/null
+++ b/python/vyos/vpp/interface/geneve.py
@@ -0,0 +1,90 @@
+# VyOS implementation of Geneve interface
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+
+def show():
+ """Show Geneve interface
+ Example:
+ from vyos.vpp.interface import geneve
+ geneve.show()
+ """
+ vpp = VPPControl()
+ return vpp.api.geneve_tunnel_dump()
+
+
+class GeneveInterface:
+ def __init__(self, ifname, source_address, remote, vni, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('geneve'))
+ self.ifname = f'geneve_tunnel{self.instance}'
+ self.src_address = source_address
+ self.dst_address = remote
+ self.vni = vni
+ self.kernel_interface = kernel_interface
+ self.vpp = VPPControl()
+
+ def add(self):
+ """Create Geneve interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/geneve/geneve.api
+
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='geneve25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.add()
+ """
+ return self.vpp.api.geneve_add_del_tunnel2(
+ is_add=True,
+ local_address=self.src_address,
+ remote_address=self.dst_address,
+ vni=self.vni,
+ l3_mode=False,
+ )
+
+ def delete(self):
+ """Delete Geneve interface
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.delete()
+ """
+ return self.vpp.api.geneve_add_del_tunnel2(
+ is_add=False,
+ local_address=self.src_address,
+ remote_address=self.dst_address,
+ vni=self.vni,
+ l3_mode=False,
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import GeneveInterface
+ a = GeneveInterface(ifname='vxlan25', source_address='192.0.2.1', remote='203.0.113.25', vni=25)
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/gre.py b/python/vyos/vpp/interface/gre.py
new file mode 100644
index 000000000..bc278d2a0
--- /dev/null
+++ b/python/vyos/vpp/interface/gre.py
@@ -0,0 +1,140 @@
+# VyOS implementation of VPP GRE interface
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+from vyos.vpp.interface.interface import Interface
+
+
+def show():
+ """Show GRE interface
+ Example:
+ from vyos.vpp.interface import gre
+ gre.show()
+ """
+ vpp = VPPControl()
+ return vpp.api.gre_tunnel_dump()
+
+
+class GREInterface(Interface):
+ """
+ Class representing a GRE (Generic Routing Encapsulation) interface.
+
+ Attributes:
+ ifname (str): The interface name.
+ source_address (str): The source IP address for the GRE tunnel.
+ remote (str): The remote IP address for the GRE tunnel.
+ tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'.
+ mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'.
+ kernel_interface (str): The associated kernel interface. Defaults to an empty string.
+ instance (int): The instance number derived from the interface name.
+ vpp (VPPControl): An instance of the VPPControl class for interacting with the VPP API.
+ """
+
+ # Mapping of tunnel types https://github.com/FDio/vpp/blob/stable/2406/src/plugins/gre/gre.api#L25-L35
+ TUNNEL_TYPE_MAP = {
+ 'l3': 0,
+ 'teb': 1,
+ 'erspan': 2,
+ }
+
+ MODE_MAP = {
+ 'point-to-point': 0,
+ 'point-to-multipoint': 1,
+ }
+
+ def __init__(
+ self,
+ ifname,
+ source_address,
+ remote,
+ tunnel_type: str = 'l3',
+ mode: str = 'point-to-point',
+ kernel_interface: str = '',
+ state: str = 'up',
+ ):
+ """
+ Initialize a GREInterface instance.
+
+ Args:
+ ifname (str): The interface name.
+ source_address (str): The source IP address for the GRE tunnel.
+ remote (str): The remote IP address for the GRE tunnel.
+ mode (str): The mode of the GRE tunnel. Options are 'point-to-point' and 'point-to-multipoint'. Defaults to 'point-to-point'.
+ tunnel_type (str): The type of GRE tunnel. Defaults to 'l3'.
+ kernel_interface (str): The associated kernel interface. Defaults to an empty string.
+ state (str): The state of the interface. Defaults to 'up'.
+ """
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('gre'))
+ self.ifname = ifname
+ self.src_address = source_address
+ self.dst_address = remote
+ self.tunnel_type = self.TUNNEL_TYPE_MAP[tunnel_type]
+ self.mode = self.MODE_MAP[mode]
+ self.kernel_interface = kernel_interface
+ self.initial_state = state
+
+ def add(self):
+ """Create GRE interface
+ https://github.com/FDio/vpp/blob/stable/2406/src/plugins/gre/gre.api
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25', tunnel_type='l3')
+ a.add()
+ """
+ self.vpp.api.gre_tunnel_add_del(
+ is_add=True,
+ tunnel={
+ 'src': self.src_address,
+ 'dst': self.dst_address,
+ 'instance': self.instance,
+ 'mode': self.mode,
+ 'type': self.tunnel_type,
+ },
+ )
+ # Set interface state
+ self.set_state(self.initial_state)
+
+ def delete(self):
+ """Delete GRE interface
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.delete()
+ """
+ return self.vpp.api.gre_tunnel_add_del(
+ is_add=False, tunnel={'src': self.src_address, 'dst': self.dst_address}
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import GREInterface
+ a = GREInterface(ifname='gre0', source_address='192.0.2.1', remote='203.0.113.25')
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/interface.py b/python/vyos/vpp/interface/interface.py
new file mode 100644
index 000000000..05c1d14d6
--- /dev/null
+++ b/python/vyos/vpp/interface/interface.py
@@ -0,0 +1,49 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+
+class Interface:
+ def __init__(self, ifname):
+ self.ifname = ifname
+ self.vpp = VPPControl()
+
+ def set_state(self, state: str):
+ """Set interface state to UP or DOWN
+ Args:
+ state (str): The state of the interface. Options are 'up' and 'down'.
+ Example:
+ from vyos.vpp.interface import Interface
+ a = Interface(ifname='eth0')
+ a.set_state(state='up')
+ """
+ if state not in ['up', 'down']:
+ raise ValueError(f"Invalid state: {state}")
+ state_flag = 1 if state == 'up' else 0
+ if_index = self.vpp.get_sw_if_index(self.ifname)
+ self.vpp.api.sw_interface_set_flags(sw_if_index=if_index, flags=state_flag)
+
+ def get_state(self):
+ """Get interface state
+ Example:
+ from vyos.vpp.interface import Interface
+ a = Interface(ifname='eth0')
+ a.get_state()
+ """
+ if_index = self.vpp.get_sw_if_index(self.ifname)
+ return self.vpp.api.sw_interface_dump(sw_if_index=if_index)[0]['flags']
diff --git a/python/vyos/vpp/interface/ipip.py b/python/vyos/vpp/interface/ipip.py
new file mode 100644
index 000000000..e6ed44537
--- /dev/null
+++ b/python/vyos/vpp/interface/ipip.py
@@ -0,0 +1,94 @@
+# VyOS implementation of VPP IPIP interface
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+from vyos.vpp.interface.interface import Interface
+
+
+def show():
+ """Show IPIP interface
+ Example:
+ from vyos.vpp.interface import ipip
+ ipip.show()
+ """
+ vpp = VPPControl()
+ return vpp.api.ipip_tunnel_dump()
+
+
+class IPIPInterface(Interface):
+ def __init__(
+ self,
+ ifname,
+ source_address,
+ remote,
+ kernel_interface: str = '',
+ state: str = 'up',
+ ):
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('ipip'))
+ self.ifname = ifname
+ self.src_address = source_address
+ self.dst_address = remote
+ self.kernel_interface = kernel_interface
+ self.initial_state = state
+
+ def add(self):
+ """Create IPIP interface
+ https://github.com/FDio/vpp/blob/stable/2310/src/vnet/ipip/ipip.api
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.add()
+ """
+ self.vpp.api.ipip_add_tunnel(
+ tunnel={
+ 'src': self.src_address,
+ 'dst': self.dst_address,
+ 'instance': self.instance,
+ },
+ )
+ # Set interface state
+ self.set_state(self.initial_state)
+
+ def delete(self):
+ """Delete IPIP interface
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.delete()
+ """
+ ipip_if_index = self.vpp.get_sw_if_index(f'ipip{self.instance}')
+ return self.vpp.api.ipip_del_tunnel(sw_if_index=ipip_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import IPIPInterface
+ a = IPIPInterface(ifname='ipip0', source_address='192.0.2.1', remote='192.0.2.5')
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/loopback.py b/python/vyos/vpp/interface/loopback.py
new file mode 100644
index 000000000..357423780
--- /dev/null
+++ b/python/vyos/vpp/interface/loopback.py
@@ -0,0 +1,72 @@
+# VyOS implementation of VPP Loopback interface
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp.interface.interface import Interface
+
+
+class LoopbackInterface(Interface):
+ """Interface Loopback"""
+
+ def __init__(self, ifname, kernel_interface: str = '', state: str = 'up'):
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('lo'))
+ self.ifname = f'loop{self.instance}'
+ self.kernel_interface = kernel_interface
+ self.initial_state = state
+
+ def add(self):
+ """Create Loopback interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/vnet/interface.api
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.add()
+ """
+ self.vpp.api.create_loopback_instance(
+ is_specified=True, user_instance=self.instance
+ )
+ # Set interface state
+ self.set_state(self.initial_state)
+
+ def delete(self):
+ """Delete Loopback interface
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.delete()
+ """
+ loopback_if_index = self.vpp.get_sw_if_index(f'loop{self.instance}')
+ return self.vpp.api.delete_loopback(sw_if_index=loopback_if_index)
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import LoopbackInterface
+ a = LoopbackInterface(ifname='lo1')
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/vxlan.py b/python/vyos/vpp/interface/vxlan.py
new file mode 100644
index 000000000..4be5b5ec9
--- /dev/null
+++ b/python/vyos/vpp/interface/vxlan.py
@@ -0,0 +1,106 @@
+# VyOS implementation of VPP VXLAN interface
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+from vyos.vpp.interface.interface import Interface
+
+
+def show():
+ """Show VXLAN interface
+ Example:
+ from vyos.vpp.interface import vxlan
+ vxlan.show()
+ """
+ vpp = VPPControl()
+ return vpp.api.vxlan_tunnel_dump()
+
+
+class VXLANInterface(Interface):
+ """Interface VXLAN"""
+
+ def __init__(
+ self,
+ ifname,
+ source_address,
+ remote,
+ vni,
+ kernel_interface: str = '',
+ state: str = 'up',
+ ):
+ super().__init__(ifname)
+ self.instance = int(ifname.removeprefix('vxlan'))
+ self.ifname = f'vxlan_tunnel{self.instance}'
+ self.src_address = source_address
+ self.dst_address = remote
+ self.vni = vni
+ self.kernel_interface = kernel_interface
+ self.initial_state = state
+
+ def add(self):
+ """Create VXLAN interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/vxlan/vxlan.api
+
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.add()
+ """
+ self.vpp.api.vxlan_add_del_tunnel_v3(
+ is_add=True,
+ src_address=self.src_address,
+ dst_address=self.dst_address,
+ vni=self.vni,
+ instance=self.instance,
+ decap_next_index=1,
+ is_l3=False,
+ )
+ # Set interface state
+ self.set_state(self.initial_state)
+
+ def delete(self):
+ """Delete VXLAN interface
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.delete()
+ """
+ return self.vpp.api.vxlan_add_del_tunnel_v3(
+ is_add=False,
+ src_address=self.src_address,
+ dst_address=self.dst_address,
+ vni=self.vni,
+ is_l3=False,
+ )
+
+ def kernel_add(self):
+ """Add LCP pair
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23, kernel_interface='vpptap10')
+ a.kernel_add()
+ """
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface)
+
+ def kernel_delete(self):
+ """Delete LCP pair
+ Example:
+ from vyos.vpp.interface import VXLANInterface
+ a = VXLANInterface(ifname='vxlan23', source_address='192.0.2.1', remote='203.0.113.23', vni=23)
+ a.kernel_delete()
+ """
+ self.vpp.lcp_pair_del(self.ifname, self.kernel_interface)
diff --git a/python/vyos/vpp/interface/wireguard.py b/python/vyos/vpp/interface/wireguard.py
new file mode 100644
index 000000000..c2d36e1b0
--- /dev/null
+++ b/python/vyos/vpp/interface/wireguard.py
@@ -0,0 +1,55 @@
+# VyOS implementation of VPP Wireguard interface
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+
+class WireguardInterface:
+ """Interface Wireguard"""
+
+ def __init__(self, ifname, listen_port=51820, kernel_interface: str = ''):
+ self.instance = int(ifname.removeprefix('wg'))
+ self.ifname = ifname
+ self.listen_port = listen_port
+ self.kernel_interface = kernel_interface
+ self.vpp = VPPControl()
+
+ def add(self):
+ """Create Wireguard interface
+ https://github.com/FDio/vpp/blob/stable/2306/src/plugins/wireguard/wireguard.api
+ Example:
+ from vyos.vpp.interface import WireguardInterface
+ a = WireguardInterface(ifname='wg5', listen_port=51820, generate_key=True)
+ a.add()
+ """
+ self.vpp.api.wireguard_interface_create(
+ generate_key=True,
+ interface={'user_instance': self.instance, 'listen_port': self.listen_port},
+ )
+ if self.kernel_interface:
+ self.vpp.lcp_pair_add(self.ifname, self.kernel_interface, 'tun')
+
+ def delete(self):
+ """Delete Wireguard interface
+ Example:
+ from vyos.vpp.interface import WireguardInterface
+ a = WireguardInterface(ifname='wg5')
+ a.delete()
+ """
+ wg_if_index = self.vpp.get_sw_if_index(f'wg{self.instance}')
+ return self.vpp.api.wireguard_interface_delete(sw_if_index=wg_if_index)
diff --git a/python/vyos/vpp/interface/xconnect.py b/python/vyos/vpp/interface/xconnect.py
new file mode 100644
index 000000000..299a61405
--- /dev/null
+++ b/python/vyos/vpp/interface/xconnect.py
@@ -0,0 +1,95 @@
+# VyOS implementation of VPP bridge interface
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+from vyos.vpp.utils import iftunnel_transform
+
+
+class XconnectInterface:
+ def __init__(
+ self,
+ ifname: str,
+ members: list = [],
+ state: str = 'up',
+ ):
+ self.ifname = ifname
+ self.members = members
+ self.vpp = VPPControl()
+ self.initial_state = state
+
+ def add_l2_xconnect(self):
+ """Add l2 cross connect
+ Args:
+ members (list): The list of the xconnect members
+ Example:
+ from vyos.vpp.interface import XconnectInterface
+ a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0'])
+ a.add_l2_xconnect()
+ """
+ interface_transform_filter = ('vxlan', 'gre')
+ first_member = self.members[0]
+ second_member = self.members[1]
+ # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX'
+ if first_member.startswith(interface_transform_filter):
+ first_member = iftunnel_transform(first_member)
+ if second_member.startswith(interface_transform_filter):
+ second_member = iftunnel_transform(second_member)
+
+ member_first_if_index = self.vpp.get_sw_if_index(first_member)
+ member_second_if_index = self.vpp.get_sw_if_index(second_member)
+ self.vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_first_if_index,
+ tx_sw_if_index=member_second_if_index,
+ enable=True,
+ )
+ self.vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_second_if_index,
+ tx_sw_if_index=member_first_if_index,
+ enable=True,
+ )
+
+ def del_l2_xconnect(self):
+ """Move l2 cross connect member to mode l3 (delte xconnect)
+ Args:
+ members (list): The list of the xconnect members
+ Example:
+ from vyos.vpp.interface import XconnectInterface
+ a = XconnectInterface(ifname='xcon0', members=['eth0', 'vxlan0'])
+ a.del_l2_xconnect()
+ """
+ interface_transform_filter = ('vxlan', 'gre')
+ first_member = self.members[0]
+ second_member = self.members[1]
+ # Check if member in required filter to transform 'vxlanX' => 'vxlan_tunnelX'
+ if first_member.startswith(interface_transform_filter):
+ first_member = iftunnel_transform(first_member)
+ if second_member.startswith(interface_transform_filter):
+ second_member = iftunnel_transform(second_member)
+
+ member_first_if_index = self.vpp.get_sw_if_index(first_member)
+ member_second_if_index = self.vpp.get_sw_if_index(second_member)
+ self.vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_first_if_index,
+ tx_sw_if_index=member_second_if_index,
+ enable=False,
+ )
+ self.vpp.api.sw_interface_set_l2_xconnect(
+ rx_sw_if_index=member_second_if_index,
+ tx_sw_if_index=member_first_if_index,
+ enable=False,
+ )
diff --git a/python/vyos/vpp/nat/__init__.py b/python/vyos/vpp/nat/__init__.py
new file mode 100644
index 000000000..16685c9cc
--- /dev/null
+++ b/python/vyos/vpp/nat/__init__.py
@@ -0,0 +1,4 @@
+from .nat44 import Nat44
+from .det44 import Det44
+
+__all__ = ['Nat44', 'Det44']
diff --git a/python/vyos/vpp/nat/det44.py b/python/vyos/vpp/nat/det44.py
new file mode 100644
index 000000000..70f903086
--- /dev/null
+++ b/python/vyos/vpp/nat/det44.py
@@ -0,0 +1,106 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+
+class Det44:
+ def __init__(self):
+ self.vpp = VPPControl()
+
+ def enable_det44_plugin(self):
+ """Enable DET44 plugin
+ Example:
+ from vyos.vpp.nat import Det44
+ det44 = Det44()
+ det44.enable_det44_plugin()
+ https://github.com/FDio/vpp/blob/stable/2410/src/plugins/nat/det44/det44.api
+ """
+ self.vpp.api.det44_plugin_enable_disable(enable=True)
+
+ def disable_det44_plugin(self):
+ """Disable DET44 plugin"""
+ self.vpp.api.det44_plugin_enable_disable(enable=False)
+
+ def add_det44_interface_outside(self, interface_out):
+ """Add DET44 outside interface"""
+ self.vpp.api.det44_interface_add_del_feature(
+ sw_if_index=self.vpp.get_sw_if_index(interface_out),
+ is_inside=False,
+ is_add=True,
+ )
+
+ def delete_det44_interface_outside(self, interface_out):
+ """Delete DET44 outside interface"""
+ self.vpp.api.det44_interface_add_del_feature(
+ sw_if_index=self.vpp.get_sw_if_index(interface_out),
+ is_inside=False,
+ is_add=False,
+ )
+
+ def add_det44_interface_inside(self, interface_in):
+ """Add DET44 inside interface"""
+ self.vpp.api.det44_interface_add_del_feature(
+ sw_if_index=self.vpp.get_sw_if_index(interface_in),
+ is_inside=True,
+ is_add=True,
+ )
+
+ def delete_det44_interface_inside(self, interface_in):
+ """Delete DET44 inside interface"""
+ self.vpp.api.det44_interface_add_del_feature(
+ sw_if_index=self.vpp.get_sw_if_index(interface_in),
+ is_inside=True,
+ is_add=False,
+ )
+
+ def add_det44_mapping(self, in_addr, in_plen, out_addr, out_plen):
+ """Add DET44 mapping"""
+ self.vpp.api.det44_add_del_map(
+ in_addr=in_addr,
+ in_plen=in_plen,
+ out_addr=out_addr,
+ out_plen=out_plen,
+ is_add=True,
+ )
+
+ def delete_det44_mapping(self, in_addr, in_plen, out_addr, out_plen):
+ """Delete DET44 mapping"""
+ self.vpp.api.det44_add_del_map(
+ in_addr=in_addr,
+ in_plen=in_plen,
+ out_addr=out_addr,
+ out_plen=out_plen,
+ is_add=False,
+ )
+
+ def set_det44_timeouts(
+ self, icmp: int, udp: int, tcp_established: int, tcp_transitory: int
+ ):
+ """Set DET44 timeouts
+ Args:
+ tcp_established (int): TCP established timeout
+ tcp_transitory (int): TCP transitory timeout
+ udp (int): UDP timeout
+ icmp (int): ICMP timeout
+ """
+ self.vpp.api.det44_set_timeouts(
+ icmp=icmp,
+ udp=udp,
+ tcp_established=tcp_established,
+ tcp_transitory=tcp_transitory,
+ )
diff --git a/python/vyos/vpp/nat/nat44.py b/python/vyos/vpp/nat/nat44.py
new file mode 100644
index 000000000..b7e2d7744
--- /dev/null
+++ b/python/vyos/vpp/nat/nat44.py
@@ -0,0 +1,229 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from vyos.vpp import VPPControl
+
+
+# NAT44 flags
+NAT_IS_NONE = 0x00
+NAT_IS_TWICE_NAT = 0x01
+NAT_IS_SELF_TWICE_NAT = 0x02
+NAT_IS_OUT2IN_ONLY = 0x04
+NAT_IS_ADDR_ONLY = 0x08
+NAT_IS_OUTSIDE = 0x10
+NAT_IS_INSIDE = 0x20
+
+NO_INTERFACE = 0xFFFFFFFF
+
+
+class Nat44:
+ def __init__(self):
+ self.vpp = VPPControl()
+
+ def enable_nat44_ed(self):
+ """Enable NAT44 endpoint dependent plugin
+ Example:
+ from vyos.vpp.nat import Nat44
+ nat44 = Nat44()
+ nat44.enable_nat44_ed()
+ https://github.com/FDio/vpp/blob/stable/2410/src/plugins/nat/nat44-ed/nat44_ed.api
+ """
+ self.vpp.api.nat44_ed_plugin_enable_disable(enable=True)
+
+ def disable_nat44_ed(self):
+ """Disable NAT44 endpoint dependent plugin"""
+ self.vpp.api.nat44_ed_plugin_enable_disable(enable=False)
+
+ def enable_nat44_ei(self):
+ """Enable NAT44 endpoint independent plugin
+ Example:
+ from vyos.vpp.nat import Nat44
+ nat44 = Nat44()
+ nat44.enable_nat44_ei()
+ """
+ self.vpp.api.nat44_ei_plugin_enable_disable(enable=True)
+
+ def add_nat44_interface_inside(self, interface_in):
+ """Add NAT44 interface"""
+ self.vpp.api.nat44_interface_add_del_feature(
+ flags=NAT_IS_INSIDE,
+ sw_if_index=self.vpp.get_sw_if_index(interface_in),
+ is_add=True,
+ )
+
+ def delete_nat44_interface_inside(self, interface_in):
+ """Delete NAT44 interface"""
+ self.vpp.api.nat44_interface_add_del_feature(
+ flags=NAT_IS_INSIDE,
+ sw_if_index=self.vpp.get_sw_if_index(interface_in),
+ is_add=False,
+ )
+
+ def add_nat44_interface_outside(self, interface_out):
+ """Add NAT44 interface"""
+ self.vpp.api.nat44_interface_add_del_feature(
+ flags=NAT_IS_OUTSIDE,
+ sw_if_index=self.vpp.get_sw_if_index(interface_out),
+ is_add=True,
+ )
+
+ def delete_nat44_interface_outside(self, interface_out):
+ """Delete NAT44 interface"""
+ self.vpp.api.nat44_interface_add_del_feature(
+ flags=NAT_IS_OUTSIDE,
+ sw_if_index=self.vpp.get_sw_if_index(interface_out),
+ is_add=False,
+ )
+
+ def add_nat44_address_range(self, addresses, twice_nat):
+ """Add NAT44 address range"""
+ if '-' not in addresses:
+ first_ip_address = last_ip_address = addresses
+ else:
+ first_ip_address, last_ip_address = addresses.split('-')
+ self.vpp.api.nat44_add_del_address_range(
+ flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE,
+ first_ip_address=first_ip_address,
+ last_ip_address=last_ip_address,
+ is_add=True,
+ )
+
+ def delete_nat44_address_range(self, addresses, twice_nat):
+ """Delete NAT44 address range"""
+ if '-' not in addresses:
+ first_ip_address = last_ip_address = addresses
+ else:
+ first_ip_address, last_ip_address = addresses.split('-')
+ self.vpp.api.nat44_add_del_address_range(
+ flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE,
+ first_ip_address=first_ip_address,
+ last_ip_address=last_ip_address,
+ is_add=False,
+ )
+
+ def add_nat44_interface_address(self, interface, twice_nat):
+ """Add NAT44 interface address"""
+ self.vpp.api.nat44_add_del_interface_addr(
+ flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE,
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ is_add=True,
+ )
+
+ def delete_nat44_interface_address(self, interface, twice_nat):
+ """Delete NAT44 interface address"""
+ self.vpp.api.nat44_add_del_interface_addr(
+ flags=NAT_IS_TWICE_NAT if twice_nat else NAT_IS_NONE,
+ sw_if_index=self.vpp.get_sw_if_index(interface),
+ is_add=False,
+ )
+
+ def add_nat44_static_mapping(
+ self,
+ local_ip,
+ external_ip,
+ local_port,
+ external_port,
+ protocol,
+ twice_nat,
+ self_twice_nat,
+ out2in,
+ pool_ip,
+ ):
+ """Add NAT44 static mapping"""
+ flags = NAT_IS_ADDR_ONLY if not (protocol or local_port) else NAT_IS_NONE
+ flags |= NAT_IS_TWICE_NAT if twice_nat else 0
+ flags |= NAT_IS_SELF_TWICE_NAT if self_twice_nat else 0
+ flags |= NAT_IS_OUT2IN_ONLY if out2in else 0
+ self.vpp.api.nat44_add_del_static_mapping_v2(
+ local_ip_address=local_ip,
+ external_ip_address=external_ip,
+ protocol=protocol,
+ local_port=local_port,
+ external_port=external_port,
+ match_pool=True if pool_ip else False,
+ pool_ip_address=pool_ip if pool_ip else '',
+ flags=flags,
+ is_add=True,
+ )
+
+ def delete_nat44_static_mapping(
+ self,
+ local_ip,
+ external_ip,
+ local_port,
+ external_port,
+ protocol,
+ twice_nat,
+ self_twice_nat,
+ out2in,
+ pool_ip,
+ ):
+ """Delete NAT44 static mapping"""
+ flags = NAT_IS_ADDR_ONLY if not (protocol or local_port) else NAT_IS_NONE
+ flags |= NAT_IS_TWICE_NAT if twice_nat else 0
+ flags |= NAT_IS_SELF_TWICE_NAT if self_twice_nat else 0
+ flags |= NAT_IS_OUT2IN_ONLY if out2in else 0
+ self.vpp.api.nat44_add_del_static_mapping_v2(
+ local_ip_address=local_ip,
+ external_ip_address=external_ip,
+ protocol=protocol,
+ local_port=local_port,
+ external_port=external_port,
+ match_pool=True if pool_ip else False,
+ pool_ip_address=pool_ip if pool_ip else '',
+ flags=flags,
+ is_add=False,
+ )
+
+ def add_nat44_identity_mapping(self, ip_address, protocol, port, interface):
+ """Add NAT44 identity mapping"""
+ self.vpp.api.nat44_add_del_identity_mapping(
+ ip_address=ip_address,
+ protocol=protocol,
+ port=port,
+ sw_if_index=(
+ self.vpp.get_sw_if_index(interface) if interface else NO_INTERFACE
+ ),
+ flags=NAT_IS_ADDR_ONLY if not (protocol or port) else NAT_IS_NONE,
+ is_add=True,
+ )
+
+ def delete_nat44_identity_mapping(self, ip_address, protocol, port, interface):
+ """Delete NAT44 identity mapping"""
+ self.vpp.api.nat44_add_del_identity_mapping(
+ ip_address=ip_address,
+ protocol=protocol,
+ port=port,
+ sw_if_index=(
+ self.vpp.get_sw_if_index(interface) if interface else NO_INTERFACE
+ ),
+ flags=NAT_IS_ADDR_ONLY if not (protocol or port) else NAT_IS_NONE,
+ is_add=False,
+ )
+
+ def set_nat_timeouts(self, icmp, udp, tcp_established, tcp_transitory):
+ """Set NAT timeouts"""
+ self.vpp.api.nat_set_timeouts(
+ icmp=icmp,
+ udp=udp,
+ tcp_established=tcp_established,
+ tcp_transitory=tcp_transitory,
+ )
+
+ def enable_ipfix(self):
+ """Enable NAT44 IPFIX logging"""
+ self.vpp.api.nat44_ei_ipfix_enable_disable(enable=True)
diff --git a/python/vyos/vpp/utils.py b/python/vyos/vpp/utils.py
new file mode 100644
index 000000000..4e42232cd
--- /dev/null
+++ b/python/vyos/vpp/utils.py
@@ -0,0 +1,417 @@
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import ctypes
+import os
+import socket
+from fcntl import ioctl
+from pathlib import Path
+from struct import pack
+
+
+mem_shift = {'K': 10, 'KB': 10, 'M': 20, 'MB': 20, 'G': 30, 'GB': 30}
+
+
+def iftunnel_transform(iface: str) -> str:
+ """Transform interface name from `xxxNN` to `xxx_tunnelNN`
+
+ Args:
+ iface (str): original interface name
+
+ Raises:
+ ValueError: Raised if an interface name does not start with a alpha and ends with decimal digit
+
+ Returns:
+ str: Transformed interface name
+ """
+ # Check format
+ if not iface[0].isascii() or not iface[-1].isdecimal():
+ raise ValueError(f'Wrong interface name format: {iface}')
+ # Transform
+ iface_type: str = iface.rstrip('0123456789')
+ iface_num: str = iface.removeprefix(iface_type)
+ # Return transformed
+ return f'{iface_type}_tunnel{iface_num}'
+
+
+def cli_ifaces_list(config_instance, mode: str = 'candidate') -> list[str]:
+ """List of all VPP interfaces (CLI names)
+
+ Args:
+ config_instance (VyOS Config): VyOS Config instance
+ mode (str, optional): `candidate` or `running`. Defaults to 'candidate'.
+
+ Returns:
+ list[str]: list of interfaces
+ """
+
+ effective_mode: bool = True if mode == 'running' else False
+
+ # Read a config
+ config = config_instance.get_config_dict(
+ ['vpp'],
+ key_mangling=('-', '_'),
+ effective=effective_mode,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ vpp_ifaces: list[str] = []
+
+ # Get a list of Ethernet interfaces
+ for iface in config.get('settings', {}).get('interface', {}).keys():
+ vpp_ifaces.append(iface)
+
+ # Get a list of VPP interfaces
+ for iface_type in config.get('interfaces', {}).keys():
+ for iface in config.get('interfaces', {}).get(iface_type, {}).keys():
+ vpp_ifaces.append(iface)
+
+ return vpp_ifaces
+
+
+def cli_ethernet_with_vifs_ifaces(config_instance) -> list[str]:
+ """List of all VPP Ethernet interfaces with VIFs
+
+ Args:
+ config_instance (VyOS Config): VyOS Config instance
+
+ Returns:
+ list[str]: list of interfaces
+ """
+ from vyos.configdict import get_interface_dict
+
+ # Read a config
+ config = config_instance.get_config_dict(
+ ['vpp'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ ifaces: list[str] = []
+
+ # Get a list of Ethernet interfaces
+ for iface in config.get('settings', {}).get('interface', {}).keys():
+ ifaces.append(iface)
+
+ # Add Ethernet interfaces with VIFs
+ for iface in ifaces:
+ _, iface_config = get_interface_dict(
+ config_instance, ['interfaces', 'ethernet'], ifname=iface
+ )
+ ifaces.extend([f'{iface}.{vif}' for vif in iface_config.get('vif', {})])
+ ifaces.extend([f'{iface}.{vif_s}' for vif_s in iface_config.get('vif_s', {})])
+
+ return ifaces
+
+
+def vpp_ifaces_list(vpp_api) -> list[dict]:
+ """List interfaces in VPP
+
+ Args:
+ vpp_api (_type_): VPP API object
+
+ Returns:
+ list[dict]: list of dictionaries with interfaces
+ """
+ ifaces_list: list[dict] = []
+ sw_ifaces_dump = vpp_api.sw_interface_dump()
+ while sw_ifaces_dump:
+ iface_details = sw_ifaces_dump.pop()
+ ifaces_list.append(iface_details._asdict())
+
+ return ifaces_list
+
+
+def vpp_ip_addresses_by_index(vpp_api, index: str) -> list[str]:
+ """List of IP addresses for interface by its index in VPP
+
+ Args:
+ vpp_api (_type_): VPP API object
+ index (str): interface index in vpp
+
+ Returns:
+ list[str]: list of IP addresses
+ """
+ ip_addresses_list: list[dict] = []
+ ip_address_dump = vpp_api.ip_address_dump(sw_if_index=index)
+ while ip_address_dump:
+ ip_address_details = ip_address_dump.pop()
+ ip_addresses_list.append(str(ip_address_details._asdict().get('prefix')))
+ return ip_addresses_list
+
+
+def vpp_ifaces_stats(
+ iface_name: str = '',
+) -> dict[str, dict[str, int | dict[str, int]]]:
+ from re import compile as re_compile
+ from vpp_papi import vpp_stats
+
+ def total_value(val_list: vpp_stats.SimpleList) -> int | dict[str, int]:
+ """Helper for aggregation stats from multiple workers
+
+ Args:
+ val_list (vpp_stats.SimpleList): list of stats for all workers
+
+ Returns:
+ int | dict[str, int]: Summary stats
+ """
+ # if all items are int return their sum
+ if all(isinstance(value, int) for value in val_list):
+ return sum(val_list)
+ # if all items are tuple
+ if all(isinstance(value, tuple) for value in val_list):
+ combined_stats = {}
+ # process individual workers
+ for worker_stats in val_list:
+ packets, octets = worker_stats
+ sum_packets = combined_stats.get('packets', 0) + packets
+ sum_octets = combined_stats.get('octets', 0) + octets
+ combined_stats = {'packets': sum_packets, 'bytes': sum_octets}
+ return combined_stats
+
+ # items are something unknown, just return what we received
+ return val_list
+
+ stats = vpp_stats.VPPStats()
+
+ ifaces_stats: dict[str, dict[str, int | dict[str, int]]] = {}
+
+ # prepare parser for stats output
+ regex_parser = re_compile(r'^/interfaces/(?P<iface>[^/]+)/(?P<param>[^/]+)')
+ # get list of available stats and dump them
+ stats_list: list[str] = stats.ls([f'^/interfaces/{iface_name}'])
+ stats_dump: list[dict[str, int]] = stats.dump(stats_list)
+
+ # parse outputs and convert it to a dictionary
+ for stats_key, stats_value in stats_dump.items():
+ parsed_key = regex_parser.search(stats_key).groupdict()
+ iface_name = parsed_key['iface']
+ param = parsed_key['param']
+ stats_item = {param: total_value(stats_value)}
+ if iface_name in ifaces_stats:
+ ifaces_stats[iface_name].update(stats_item)
+ else:
+ ifaces_stats[iface_name] = stats_item
+
+ return ifaces_stats
+
+
+def cli_ifaces_lcp_kernel_list(
+ config_instance, mode: str = 'candidate'
+) -> list[tuple[str, str]]:
+ """List of all VPP kernel-interfaces (CLI names, attached VPP interfaces)
+
+ Args:
+ config_instance (VyOS Config): VyOS Config instance
+ mode (str, optional): `candidate` or `running`. Defaults to 'candidate'.
+
+ Returns:
+ list[tuple[str, str]]: list of interfaces ([(vpp_iface, kernel_iface)])
+ """
+
+ effective_mode: bool = True if mode == 'running' else False
+
+ # Read a config
+ config = config_instance.get_config_dict(
+ ['vpp'],
+ key_mangling=('-', '_'),
+ effective=effective_mode,
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ )
+
+ lcp_kernel_ifaces: list[tuple[str, str]] = []
+
+ # Get a list with kernel interfaces
+ for ifaces_list in config.get('interfaces', {}).values():
+ for iface_name, iface_settings in ifaces_list.items():
+ if 'kernel_interface' in iface_settings:
+ lcp_kernel_ifaces.append(
+ (iface_name, iface_settings['kernel_interface'])
+ )
+
+ return lcp_kernel_ifaces
+
+
+def get_default_hugepage_size() -> int:
+ """
+ Retrieve the system's default huge page size.
+ :return: The default huge page size in bytes.
+ """
+ page_size = None
+ try:
+ # default huge page size
+ memfd = os.memfd_create('tmp', os.MFD_HUGETLB)
+ st = os.fstat(memfd)
+ page_size = st.st_blksize
+ os.close(memfd)
+ except OSError:
+ pass
+
+ return page_size
+
+
+def get_default_page_size() -> int:
+ """
+ Retrieve the system's default page size.
+ :return: The default page size in bytes.
+ """
+ return os.sysconf('SC_PAGESIZE')
+
+
+def get_hugepage_sizes() -> list[int]:
+ """
+ Retrieve all available huge page sizes from the system.
+ :return: A list of huge page sizes in bytes.
+ """
+ huge_sizes = []
+ path = '/sys/kernel/mm/hugepages/'
+ try:
+ entries = os.listdir(path)
+ for entry in entries:
+ if entry.startswith('hugepages-'):
+ try:
+ size_kb = int(entry.replace('hugepages-', '').replace('kB', ''))
+ huge_sizes.append(size_kb << 10) # Convert KB to bytes
+ except ValueError:
+ pass
+ except FileNotFoundError:
+ pass
+
+ return huge_sizes
+
+
+def human_memory_to_bytes(value: str) -> int:
+ """
+ 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:
+ 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:
+ """
+ Convert a byte value to a human-readable format (K, M, G).
+
+ :param value: The size in bytes.
+ :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
+
+
+def human_page_memory_to_bytes(value: str) -> int:
+ """
+ Convert a human-readable vpp page size format to a byte value.
+
+ :param value: The string memory size in vpp human-readable format.
+ :return: A int representing the value.
+ """
+ default = {
+ 'default': get_default_page_size,
+ 'default-hugepage': get_default_hugepage_size,
+ }
+ try:
+ return default[value]()
+ except KeyError:
+ return human_memory_to_bytes(value)
+
+
+class EthtoolGDrvinfo:
+ """Return interface details like `ethtol -i` does"""
+
+ # TODO
+ # this probably need to be replaced with a code generator
+ # like ctypeslib or C extension
+ class EthtoolDrvinfo(ctypes.Structure):
+ _fields_ = [
+ ('cmd', ctypes.c_uint32),
+ ('driver', ctypes.c_char * 32), # Driver short name
+ ('version', ctypes.c_char * 32), # Driver version
+ ('fw_version', ctypes.c_char * 32), # Firmware version
+ # Be careful: bus info can be longer than 32 chars and thus truncated
+ ('bus_info', ctypes.c_char * 32), # Bus info.
+ ('erom_version', ctypes.c_char * 32), # Expansion ROM version
+ ('reserved2', ctypes.c_char * 12), # Reserved for future use
+ ('n_priv_flags', ctypes.c_uint32), # Number of private flags
+ ('n_stats', ctypes.c_uint32), # Number of U64 stats
+ ('testinfo_len', ctypes.c_uint32), # Test info length
+ ('eedump_len', ctypes.c_uint32), # EEPROM dump length
+ ('regdump_len', ctypes.c_uint32), # Register dump length
+ ]
+
+ def __init__(self, iface: str):
+ # Constants for ethtool
+ SIOCETHTOOL = 0x8946 # pretend to be ethtool
+ ETHTOOL_GDRVINFO = 0x00000003 # Command to get driver info
+
+ # Create a dummy socket
+ sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ # Prepare the request for getting driver info
+ drvinfo = self.EthtoolDrvinfo(cmd=ETHTOOL_GDRVINFO)
+ ifreq: bytes = pack('16sP', iface.encode('utf-8'), ctypes.addressof(drvinfo))
+
+ # Make an ioctl call to get the driver info
+ try:
+ ioctl(sockfd, SIOCETHTOOL, ifreq)
+ except OSError:
+ raise FileNotFoundError(f'There is no Ethernet device: {iface}')
+
+ # Close the socket
+ sockfd.close()
+
+ # save the information
+ self.driver: str = drvinfo.driver.decode('utf-8').strip('\x00')
+ self.version: str = drvinfo.version.decode('utf-8').strip('\x00')
+ self.fw_version: str = drvinfo.fw_version.decode('utf-8').strip('\x00')
+ self.bus_info: str = drvinfo.bus_info.decode('utf-8').strip('\x00')
+ self.erom_version: str = drvinfo.erom_version.decode('utf-8').strip('\x00')
+ self.reserved2: str = drvinfo.reserved2.decode('utf-8').strip('\x00')
+ self.n_priv_flags: int = drvinfo.n_priv_flags
+ self.testinfo_len: int = drvinfo.testinfo_len
+ self.eedump_len: int = drvinfo.eedump_len
+ self.regdump_len: int = drvinfo.regdump_len
+
+ def bus_info_expand(self, bus_name: str) -> str:
+ bus_path = Path(f'/sys/bus/{bus_name}/devices').glob(f'{self.bus_info}*')
+ dev_ids = list(bus_path)
+ if not dev_ids:
+ raise FileNotFoundError(
+ f'No matching IDs on the bus: {self.bus_info} on {bus_name}'
+ )
+ if len(dev_ids) > 1:
+ raise FileNotFoundError(
+ f'There are more than one matching IDs on the bus: {dev_ids} on {bus_name}'
+ )
+ return dev_ids[0].name
diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py
new file mode 100644
index 000000000..4b79073ab
--- /dev/null
+++ b/python/vyos/vyconf_session.py
@@ -0,0 +1,233 @@
+# Copyright 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/>.
+#
+#
+
+import os
+import tempfile
+from functools import wraps
+from typing import Type
+
+from vyos.proto import vyconf_client
+from vyos.migrate import ConfigMigrate
+from vyos.migrate import ConfigMigrateError
+from vyos.component_version import append_system_version
+from vyos.utils.session import in_config_session
+from vyos.proto.vyconf_proto import Errnum
+from vyos.utils.commit import acquire_commit_lock_file
+from vyos.utils.commit import release_commit_lock_file
+from vyos.utils.commit import call_commit_hooks
+from vyos.remote import get_config_file
+
+
+class VyconfSessionError(Exception):
+ pass
+
+
+class VyconfSession:
+ def __init__(
+ self, token: str = None, pid: int = None, on_error: Type[Exception] = None
+ ):
+ self.pid = os.getpid() if pid is None else pid
+ if token is None:
+ # CLI applications with arg pid=getppid() allow coordination
+ # with the ambient session; other uses (such as ConfigSession)
+ # may default to self pid
+ out = vyconf_client.send_request('session_of_pid', client_pid=self.pid)
+ if out.output is None:
+ out = vyconf_client.send_request('setup_session', client_pid=self.pid)
+ self.__token = out.output
+ else:
+ out = vyconf_client.send_request(
+ 'session_update_pid', token=token, client_pid=self.pid
+ )
+ if out.status:
+ raise ValueError(f'No existing session for token: {token}')
+ self.__token = token
+
+ self.in_config_session = in_config_session()
+ if self.in_config_session:
+ out = vyconf_client.send_request(
+ 'enter_configuration_mode', token=self.__token
+ )
+ if out.status:
+ raise VyconfSessionError(self.output(out))
+
+ self.on_error = on_error
+
+ def teardown(self):
+ vyconf_client.send_request('teardown', token=self.__token)
+
+ def exit_config_mode(self):
+ if self.session_changed():
+ return 'Uncommited changes', Errnum.UNCOMMITED_CHANGES
+ out = vyconf_client.send_request('exit_configuration_mode', token=self.__token)
+ return self.output(out), out.status
+
+ def in_session(self) -> bool:
+ return self.in_config_session
+
+ def session_changed(self) -> bool:
+ out = vyconf_client.send_request('session_changed', token=self.__token)
+ return not bool(out.status)
+
+ def get_config(self):
+ out = vyconf_client.send_request('get_config', token=self.__token)
+ if out.status:
+ raise VyconfSessionError(self.output(out))
+ return out.output
+
+ @staticmethod
+ def config_mode(f):
+ @wraps(f)
+ def wrapped(self, *args, **kwargs):
+ msg = 'operation not available outside of config mode'
+ if not self.in_config_session:
+ if self.on_error is None:
+ raise VyconfSessionError(msg)
+ raise self.on_error(msg)
+ return f(self, *args, **kwargs)
+
+ return wrapped
+
+ @staticmethod
+ def raise_exception(f):
+ @wraps(f)
+ def wrapped(self, *args, **kwargs):
+ if self.on_error is None:
+ return f(self, *args, **kwargs)
+ o, e = f(self, *args, **kwargs)
+ if e:
+ raise self.on_error(o)
+ return o, e
+
+ return wrapped
+
+ @staticmethod
+ def output(o):
+ out = ''
+ for res in (o.output, o.error, o.warning):
+ if res is not None:
+ out = out + res
+ return out
+
+ @raise_exception
+ @config_mode
+ def set(self, path: list[str]) -> tuple[str, int]:
+ out = vyconf_client.send_request('set', token=self.__token, path=path)
+ return self.output(out), out.status
+
+ @raise_exception
+ @config_mode
+ def delete(self, path: list[str]) -> tuple[str, int]:
+ out = vyconf_client.send_request('delete', token=self.__token, path=path)
+ return self.output(out), out.status
+
+ @raise_exception
+ @config_mode
+ def commit(self) -> tuple[str, int]:
+ if not self.session_changed():
+ out = 'No changes to commit'
+ return out, 0
+
+ lock_fd, out = acquire_commit_lock_file()
+ if lock_fd is None:
+ return out, Errnum.COMMIT_IN_PROGRESS
+
+ pre_out, _ = call_commit_hooks('pre')
+ out = vyconf_client.send_request('commit', token=self.__token)
+ os.environ['COMMIT_STATUS'] = 'FAILURE' if out.status else 'SUCCESS'
+ post_out, _ = call_commit_hooks('post')
+
+ release_commit_lock_file(lock_fd)
+
+ return pre_out + self.output(out) + post_out, out.status
+
+ @raise_exception
+ @config_mode
+ def discard(self) -> tuple[str, int]:
+ out = vyconf_client.send_request('discard', token=self.__token)
+ return self.output(out), out.status
+
+ @raise_exception
+ @config_mode
+ def load_config(
+ self, file_name: str, migrate: bool = False, cached: bool = False
+ ) -> tuple[str, int]:
+ # pylint: disable=consider-using-with
+ file_path = tempfile.NamedTemporaryFile(delete=False).name
+ err = get_config_file(file_name, file_path)
+ if err:
+ os.remove(file_path)
+ return str(err), Errnum.INVALID_VALUE
+ if not cached:
+ if migrate:
+ config_migrate = ConfigMigrate(file_path)
+ try:
+ config_migrate.run()
+ except ConfigMigrateError as e:
+ os.remove(file_path)
+ return repr(e), 1
+
+ out = vyconf_client.send_request(
+ 'load', token=self.__token, location=file_path, cached=cached
+ )
+
+ if not cached:
+ os.remove(file_path)
+
+ return self.output(out), out.status
+
+ @raise_exception
+ @config_mode
+ def merge_config(
+ self, file_name: str, migrate: bool = False, destructive: bool = False
+ ) -> tuple[str, int]:
+ # pylint: disable=consider-using-with
+ file_path = tempfile.NamedTemporaryFile(delete=False).name
+ err = get_config_file(file_name, file_path)
+ if err:
+ os.remove(file_path)
+ return str(err), Errnum.INVALID_VALUE
+ if migrate:
+ config_migrate = ConfigMigrate(file_path)
+ try:
+ config_migrate.run()
+ except ConfigMigrateError as e:
+ os.remove(file_path)
+ return repr(e), 1
+
+ out = vyconf_client.send_request(
+ 'merge', token=self.__token, location=file_path, destructive=destructive
+ )
+
+ os.remove(file_path)
+
+ return self.output(out), out.status
+
+ @raise_exception
+ def save_config(self, file: str, append_version: bool = False) -> tuple[str, int]:
+ file = os.path.realpath(file)
+ out = vyconf_client.send_request('save', token=self.__token, location=file)
+ if append_version:
+ append_system_version(file)
+ return self.output(out), out.status
+
+ @raise_exception
+ def show_config(self, path: list[str] = None) -> tuple[str, int]:
+ if path is None:
+ path = []
+ out = vyconf_client.send_request('show_config', token=self.__token, path=path)
+ return self.output(out), out.status
diff --git a/python/vyos/wanloadbalance.py b/python/vyos/wanloadbalance.py
new file mode 100644
index 000000000..158ee76c8
--- /dev/null
+++ b/python/vyos/wanloadbalance.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from vyos.defaults import directories
+from vyos.utils.process import run
+
+dhclient_lease = 'dhclient_{0}.lease'
+
+def nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False):
+ output = []
+
+ if 'inbound_interface' in rule_conf:
+ ifname = rule_conf['inbound_interface']
+ if local and not exclude:
+ output.append(f'oifname != "{ifname}"')
+ elif not local:
+ output.append(f'iifname "{ifname}"')
+
+ if 'protocol' in rule_conf and rule_conf['protocol'] != 'all':
+ protocol = rule_conf['protocol']
+ operator = ''
+
+ if protocol[:1] == '!':
+ operator = '!='
+ protocol = protocol[1:]
+
+ if protocol == 'tcp_udp':
+ protocol = '{ tcp, udp }'
+
+ output.append(f'meta l4proto {operator} {protocol}')
+
+ for direction in ['source', 'destination']:
+ if direction not in rule_conf:
+ continue
+
+ direction_conf = rule_conf[direction]
+ prefix = direction[:1]
+
+ if 'address' in direction_conf:
+ operator = ''
+ address = direction_conf['address']
+ if address[:1] == '!':
+ operator = '!='
+ address = address[1:]
+ output.append(f'ip {prefix}addr {operator} {address}')
+
+ if 'port' in direction_conf:
+ operator = ''
+ port = direction_conf['port']
+ if port[:1] == '!':
+ operator = '!='
+ port = port[1:]
+ output.append(f'th {prefix}port {operator} {{ {port} }}')
+
+ if 'source_based_routing' not in rule_conf and not restore_mark:
+ output.append('ct state new')
+
+ if limit and 'limit' in rule_conf and 'rate' in rule_conf['limit']:
+ output.append(f'limit rate {rule_conf["limit"]["rate"]}/{rule_conf["limit"]["period"]}')
+ if 'burst' in rule_conf['limit']:
+ output.append(f'burst {rule_conf["limit"]["burst"]} packets')
+
+ output.append('counter')
+
+ if restore_mark:
+ output.append('meta mark set ct mark')
+ elif weight:
+ weights, total_weight = wlb_weight_interfaces(rule_conf, health_state)
+ if len(weights) > 1: # Create weight-based verdict map
+ vmap_str = ", ".join(f'{weight} : jump wlb_mangle_isp_{ifname}' for ifname, weight in weights)
+ output.append(f'numgen random mod {total_weight} vmap {{ {vmap_str} }}')
+ elif len(weights) == 1: # Jump to single ISP
+ ifname, _ = weights[0]
+ output.append(f'jump wlb_mangle_isp_{ifname}')
+ else: # No healthy interfaces
+ return ""
+ elif action:
+ output.append(action)
+
+ return " ".join(output)
+
+def wlb_weight_interfaces(rule_conf, health_state):
+ interfaces = []
+
+ for ifname, if_conf in rule_conf['interface'].items():
+ if ifname in health_state and health_state[ifname]['state']:
+ weight = int(if_conf.get('weight', 1))
+ interfaces.append((ifname, weight))
+
+ if not interfaces:
+ return [], 0
+
+ if 'failover' in rule_conf:
+ for ifpair in sorted(interfaces, key=lambda i: i[1], reverse=True):
+ return [ifpair], ifpair[1] # Return highest weight interface that is ACTIVE when in failover
+
+ total_weight = sum(weight for _, weight in interfaces)
+ out = []
+ start = 0
+ for ifname, weight in sorted(interfaces, key=lambda i: i[1]): # build weight ranges
+ end = start + weight - 1
+ out.append((ifname, f'{start}-{end}' if end > start else start))
+ start = weight
+
+ return out, total_weight
+
+def health_ping_host(host, ifname, count=1, wait_time=0):
+ cmd_str = f'ping -c {count} -W {wait_time} -I {ifname} {host}'
+ rc = run(cmd_str)
+ return rc == 0
+
+def health_ping_host_ttl(host, ifname, count=1, ttl_limit=0):
+ cmd_str = f'ping -c {count} -t {ttl_limit} -I {ifname} {host}'
+ rc = run(cmd_str)
+ return rc != 0
+
+def parse_dhcp_nexthop(ifname):
+ lease_file = os.path.join(directories['isc_dhclient_dir'], dhclient_lease.format(ifname))
+
+ if not os.path.exists(lease_file):
+ return False
+
+ with open(lease_file, 'r') as f:
+ for line in f.readlines():
+ data = line.replace('\n', '').split('=')
+ if data[0] == 'new_routers':
+ return data[1].replace("'", '').split(" ")[0]
+
+ return None
+
+def parse_ppp_nexthop(ifname):
+ nexthop_file = os.path.join(directories['ppp_nexthop_dir'], ifname)
+
+ if not os.path.exists(nexthop_file):
+ return False
+
+ with open(nexthop_file, 'r') as f:
+ return f.read()
diff --git a/python/vyos/xml_ref/__init__.py b/python/vyos/xml_ref/__init__.py
index 99d8432d2..41a25049e 100644
--- a/python/vyos/xml_ref/__init__.py
+++ b/python/vyos/xml_ref/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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,6 +14,8 @@
# along with this library. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional, Union, TYPE_CHECKING
+from typing import Callable
+from typing import Any
from vyos.xml_ref import definition
from vyos.xml_ref import op_definition
@@ -89,6 +91,7 @@ def from_source(d: dict, path: list) -> bool:
def ext_dict_merge(source: dict, destination: Union[dict, 'ConfigDict']):
return definition.ext_dict_merge(source, destination)
+
def load_op_reference(op_cache=[]):
if op_cache:
return op_cache[0]
@@ -108,5 +111,26 @@ def load_op_reference(op_cache=[]):
return op_xml
-def get_op_ref_path(path: list) -> list[op_definition.PathData]:
- return load_op_reference()._get_op_ref_path(path)
+
+def walk_op_data(func: Callable[[tuple, dict], Any]):
+ return load_op_reference().walk(func)
+
+
+def walk_op_node_data():
+ return load_op_reference().walk_node_data()
+
+
+def lookup_op_data(
+ path: list, tag_values: bool = False, last_node_type: str = ''
+) -> (dict, list[str]):
+ return load_op_reference().lookup(
+ path, tag_values=tag_values, last_node_type=last_node_type
+ )
+
+
+def lookup_op_node_data(
+ path: list, tag_values: bool = False, last_node_type: str = ''
+) -> list[op_definition.NodeData]:
+ return load_op_reference().lookup_node_data(
+ path, tag_values=tag_values, last_node_type=last_node_type
+ )
diff --git a/python/vyos/xml_ref/definition.py b/python/vyos/xml_ref/definition.py
index 4e755ab72..015e7ee6e 100644
--- a/python/vyos/xml_ref/definition.py
+++ b/python/vyos/xml_ref/definition.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
diff --git a/python/vyos/xml_ref/generate_cache.py b/python/vyos/xml_ref/generate_cache.py
index 093697993..f0a3ec35b 100755
--- a/python/vyos/xml_ref/generate_cache.py
+++ b/python/vyos/xml_ref/generate_cache.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
diff --git a/python/vyos/xml_ref/generate_op_cache.py b/python/vyos/xml_ref/generate_op_cache.py
index cd2ac890e..f49b2290b 100755
--- a/python/vyos/xml_ref/generate_op_cache.py
+++ b/python/vyos/xml_ref/generate_op_cache.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,10 +14,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
+import io
import re
import sys
-import json
import glob
+import json
+import atexit
from argparse import ArgumentParser
from os.path import join
@@ -25,23 +28,43 @@ from os.path import abspath
from os.path import dirname
from xml.etree import ElementTree as ET
from xml.etree.ElementTree import Element
+from functools import cmp_to_key
from typing import TypeAlias
from typing import Optional
+from op_definition import NodeData
+from op_definition import OpKey # pylint: disable=unused-import # noqa: F401
+from op_definition import OpData # pylint: disable=unused-import # noqa: F401
+from op_definition import key_name
+from op_definition import key_type
+from op_definition import node_data_difference
+from op_definition import get_node_data
+from op_definition import collapse
+
_here = dirname(__file__)
sys.path.append(join(_here, '..'))
-from defaults import directories
+# pylint: disable=wrong-import-position,wrong-import-order
+from defaults import directories # noqa: E402
-from op_definition import NodeData
-from op_definition import PathData
-xml_op_cache_json = 'xml_op_cache.json'
-xml_op_tmp = join('/tmp', xml_op_cache_json)
op_ref_cache = abspath(join(_here, 'op_cache.py'))
OptElement: TypeAlias = Optional[Element]
-DEBUG = False
+
+
+# It is expected that the node_data help txt contained in top-level nodes,
+# shared across files, e.g.'show', will reveal inconsistencies; to list
+# differences, use --check-xml-consistency
+CHECK_XML_CONSISTENCY = False
+err_buf = io.StringIO()
+
+
+def write_err_buf():
+ err_buf.seek(0)
+ out = err_buf.read()
+ print(out)
+ err_buf.close()
def translate_exec(s: str) -> str:
@@ -57,7 +80,7 @@ def translate_position(s: str, pos: list[str]) -> str:
# preferred to .format(*list) to avoid collisions with braces
for i, p in enumerate(pos):
- t = t.replace(f'_place_holder_{i+1}_', p)
+ t = t.replace(f'_place_holder_{i+1}_', f'{{{{{p}}}}}')
return t
@@ -65,6 +88,14 @@ def translate_position(s: str, pos: list[str]) -> str:
def translate_command(s: str, pos: list[str]) -> str:
s = translate_exec(s)
s = translate_position(s, pos)
+
+ # If there are any untranslated occurences of '_place_holder_",
+ # it means the command is incorrect,
+ # e.g., it references "$6" when it only has five words.
+ if re.search(r'_place_holder_', s):
+ print(f'Command translation failed: {s}')
+ sys.exit(1)
+
return s
@@ -74,101 +105,245 @@ def translate_op_script(s: str) -> str:
return s
-def insert_node(n: Element, l: list[PathData], path = None) -> None:
- # pylint: disable=too-many-locals,too-many-branches
+def compare_keys(a, b):
+ # pylint: disable=too-many-return-statements
+ match key_type(a), key_type(b):
+ case None, None:
+ if key_name(a) == key_name(b):
+ return 0
+ return -1 if key_name(a) < key_name(b) else 1
+ case None, _:
+ return -1
+ case _, None:
+ return 1
+ case _, _:
+ if key_name(a) == key_name(b):
+ if key_type(a) == key_type(b):
+ return 0
+ return -1 if key_type(a) < key_type(b) else 1
+ return -1 if key_name(a) < key_name(b) else 1
+
+
+def sort_func(obj: dict, key_func):
+ if not obj or not isinstance(obj, dict):
+ return obj
+ k_list = list(obj.keys())
+ if not isinstance(k_list[0], tuple):
+ return obj
+ k_list = sorted(k_list, key=key_func)
+ v_list = map(lambda t: sort_func(obj[t], key_func), k_list)
+ return dict(zip(k_list, v_list))
+
+
+def sort_op_data(obj):
+ key_func = cmp_to_key(compare_keys)
+ return sort_func(obj, key_func)
+
+
+def insert_node(
+ n: Element, d: dict, path: list[str] = None, parent: NodeData = None, file: str = ''
+) -> None:
+ # pylint: disable=too-many-locals,too-many-branches,too-many-statements
prop: OptElement = n.find('properties')
children: OptElement = n.find('children')
command: OptElement = n.find('command')
- # name is not None as required by schema
- name: str = n.get('name', 'schema_error')
+ standalone: OptElement = n.find('standalone')
node_type: str = n.tag
+
+ if node_type == 'virtualTagNode':
+ name = '__virtual_tag'
+ else:
+ name = n.get('name')
+ if not name:
+ raise ValueError(
+ 'Node name is required for all node types except <virtualTagNode>'
+ )
+
if path is None:
path = []
- path.append(name)
+ if node_type != 'virtualTagNode':
+ path.append(name)
+
if node_type == 'tagNode':
path.append(f'{name}-tag_value')
+ if node_type == 'virtualTagNode':
+ path.append(f'{parent.name}-tag_value')
+
help_prop: OptElement = None if prop is None else prop.find('help')
help_text = None if help_prop is None else help_prop.text
command_text = None if command is None else command.text
if command_text is not None:
command_text = translate_command(command_text, path)
- comp_help = None
+ try:
+ standalone_command = translate_command(standalone.find('command').text, path)
+ except AttributeError:
+ standalone_command = None
+
+ try:
+ standalone_help_text = translate_command(standalone.find('help').text, path)
+ except AttributeError:
+ standalone_help_text = None
+
+ comp_help = {}
if prop is not None:
- che = prop.findall("completionHelp")
+ che = prop.findall('completionHelp')
+
for c in che:
- lists = c.findall("list")
- paths = c.findall("path")
- scripts = c.findall("script")
-
- comp_help = {}
- list_l = []
- for i in lists:
- list_l.append(i.text)
- path_l = []
- for i in paths:
- path_str = re.sub(r'\s+', '/', i.text)
- path_l.append(path_str)
- script_l = []
- for i in scripts:
- script_str = translate_op_script(i.text)
- script_l.append(script_str)
-
- comp_help['list'] = list_l
- comp_help['fs_path'] = path_l
- comp_help['script'] = script_l
-
- for d in l:
- if name in list(d):
- break
- else:
- d = {}
- l.append(d)
+ comp_list_els = c.findall('list')
+ comp_path_els = c.findall('path')
+ comp_script_els = c.findall('script')
+
+ comp_lists = []
+ for i in comp_list_els:
+ comp_lists.append(i.text)
- inner_l = d.setdefault(name, [])
+ comp_paths = []
+ for i in comp_path_els:
+ comp_paths.append(i.text)
- inner_d: PathData = {'node_data': NodeData(node_type=node_type,
- help_text=help_text,
- comp_help=comp_help,
- command=command_text,
- path=path)}
- inner_l.append(inner_d)
+ comp_scripts = []
+ for i in comp_script_els:
+ comp_script_str = translate_op_script(i.text)
+ comp_scripts.append(comp_script_str)
+
+ if comp_lists:
+ comp_help['list'] = comp_lists
+ if comp_paths:
+ comp_help['path'] = comp_paths
+ if comp_scripts:
+ comp_help['script'] = comp_scripts
+
+ new_node_data = NodeData()
+ new_node_data.name = name
+ new_node_data.node_type = node_type
+ new_node_data.comp_help = comp_help
+ new_node_data.help_text = help_text
+ new_node_data.command = command_text
+ new_node_data.standalone_help_text = standalone_help_text
+ new_node_data.standalone_command = standalone_command
+ new_node_data.path = path
+ new_node_data.files = [file]
+
+ value = {('__node_data', None): new_node_data}
+ key = (name, node_type)
+
+ cur_value = d.setdefault(key, value)
+
+ if CHECK_XML_CONSISTENCY:
+ out = node_data_difference(get_node_data(cur_value), get_node_data(value))
+ if out:
+ err_buf.write(out)
+
+ # track the correct pointer reference:
+ cur_node_data = cur_value[('__node_data', None)]
+
+ if file not in cur_node_data.files:
+ cur_node_data.files.append(file)
+
+ if not cur_node_data.comp_help and comp_help:
+ cur_node_data.comp_help = comp_help
+
+ if not cur_node_data.help_text and help_text:
+ cur_node_data.help_text = help_text
+
+ if not cur_node_data.command and command_text:
+ cur_node_data.command = command_text
+
+ if not cur_node_data.standalone_help_text and standalone_help_text:
+ cur_node_data.standalone_help_text = standalone_help_text
+
+ if not cur_node_data.standalone_command and standalone_command:
+ cur_node_data.standalone_command = standalone_command
+
+ if parent and key not in parent.children:
+ parent.children.append(key)
if children is not None:
- inner_nodes = children.iterfind("*")
+ inner_nodes = children.iterfind('*')
for inner_n in inner_nodes:
inner_path = path[:]
- insert_node(inner_n, inner_l, inner_path)
+ insert_node(inner_n, d[key], inner_path, cur_node_data, file)
-def parse_file(file_path, l):
+def parse_file(file_path, d):
tree = ET.parse(file_path)
root = tree.getroot()
- for n in root.iterfind("*"):
- insert_node(n, l)
+ file = os.path.basename(file_path)
+ for n in root.iterfind('*'):
+ insert_node(n, d, file=file)
def main():
+ # pylint: disable=global-statement
+ global CHECK_XML_CONSISTENCY
+
parser = ArgumentParser(description='generate dict from xml defintions')
- parser.add_argument('--xml-dir', type=str, required=True,
- help='transcluded xml op-mode-definition file')
+ parser.add_argument(
+ '--xml-dir',
+ type=str,
+ required=True,
+ help='transcluded xml op-mode-definition file',
+ )
+ parser.add_argument(
+ '--check-xml-consistency',
+ action='store_true',
+ help='check consistency of node data across files',
+ )
+ parser.add_argument(
+ '--select',
+ type=str,
+ help='limit cache to a subset of XML files: "power_ctl | multicast-group | ..."',
+ )
+
+ parser.add_argument(
+ '--export-json',
+ type=str,
+ help='Export a JSON version of the cache to a file',
+ )
args = vars(parser.parse_args())
+ if args['check_xml_consistency']:
+ CHECK_XML_CONSISTENCY = True
+ atexit.register(write_err_buf)
+
xml_dir = abspath(args['xml_dir'])
- l = []
+ op_mode_data = {}
- for fname in glob.glob(f'{xml_dir}/*.xml'):
- parse_file(fname, l)
+ select = args['select']
+ if select:
+ select = [item.strip() for item in select.split('|')]
- with open(xml_op_tmp, 'w') as f:
- json.dump(l, f, indent=2)
+ for fname in sorted(glob.glob(f'{xml_dir}/*.xml')):
+ file = os.path.basename(fname)
+ if not select or os.path.splitext(file)[0] in select:
+ parse_file(fname, op_mode_data)
+
+ op_mode_data = sort_op_data(op_mode_data)
+
+ res, out, err = collapse(op_mode_data)
+ if err:
+ print(
+ 'Failed to generate operational command definition cache due to duplicate paths.'
+ )
+ print('Found the following duplicate paths:\n')
+ print(out)
+ sys.exit(1)
+ else:
+ op_mode_data = res
with open(op_ref_cache, 'w') as f:
- f.write(f'op_reference = {str(l)}')
+ f.write('from vyos.xml_ref.op_definition import NodeData\n')
+ f.write(f'op_reference = {str(op_mode_data)}')
+
+ if args['export_json']:
+ with open(args['export_json'], 'w') as f:
+ json.dump(op_mode_data, f)
+
if __name__ == '__main__':
main()
diff --git a/python/vyos/xml_ref/op_definition.py b/python/vyos/xml_ref/op_definition.py
index 914f3a105..d4b767324 100644
--- a/python/vyos/xml_ref/op_definition.py
+++ b/python/vyos/xml_ref/op_definition.py
@@ -1,4 +1,4 @@
-# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 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
@@ -13,37 +13,244 @@
# 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 typing import TypedDict
from typing import TypeAlias
-from typing import Optional
from typing import Union
+from typing import Optional
+from typing import Iterator
+from dataclasses import dataclass
+from dataclasses import field
+from dataclasses import fields
+from dataclasses import asdict
+from itertools import filterfalse
+
+
+@dataclass
+class NodeData:
+ # pylint: disable=too-many-instance-attributes
+ name: str = ''
+ node_type: str = 'node'
+ help_text: str = ''
+ comp_help: dict[str, list] = field(default_factory=dict)
+ command: str = ''
+ standalone_help_text: Optional[str] = None
+ standalone_command: Optional[str] = None
+ path: list[str] = field(default_factory=list)
+ files: list[str] = field(default_factory=list)
+ children: list[tuple] = field(default_factory=list)
+
+
+OpKey: TypeAlias = tuple[str, str]
+OpData: TypeAlias = dict[OpKey, Union[NodeData, 'OpData']]
+
+
+def key_name(k: OpKey):
+ return k[0]
+
+
+def key_type(k: OpKey):
+ return k[1]
+
+
+def key_names(l: list): # noqa: E741
+ return list(map(lambda t: t[0], l))
+
+
+def keys_of_name(s: str, l: list): # noqa: E741
+ filter(lambda t: t[0] == s, l)
+
+
+def is_tag_node(t: tuple):
+ return t[1] == 'tagNode'
+
+
+def subdict_of_name(s: str, d: dict) -> dict:
+ res = {}
+ for t, v in d.items():
+ if not isinstance(t, tuple):
+ break
+ if key_name(t) == s:
+ res[t] = v
+
+ return res
+
+
+def next_keys(d: dict) -> list:
+ key_set = set()
+ for k in list(d.keys()):
+ if isinstance(d[k], dict):
+ key_set |= set(d[k].keys())
+ return list(key_set)
+
+
+def tuple_paths(d: dict) -> Iterator[list[tuple]]:
+ def func(d, path):
+ if isinstance(d, dict):
+ if not d:
+ yield path
+ for k, v in d.items():
+ if isinstance(k, tuple) and key_name(k) != '__node_data':
+ for r in func(v, path + [k]):
+ yield r
+ else:
+ yield path
+ else:
+ yield path
+ for r in func(d, []):
+ yield r
-class NodeData(TypedDict):
- node_type: Optional[str]
- help_text: Optional[str]
- comp_help: Optional[dict[str, list]]
- command: Optional[str]
- path: Optional[list[str]]
+def match_tuple_paths(
+ path: list[str], paths: list[list[tuple[str, str]]]
+) -> list[list[tuple[str, str]]]:
+ return list(filter(lambda p: key_names(p) == path, paths))
-PathData: TypeAlias = dict[str, Union[NodeData|list['PathData']]]
+
+def get_node_data(d: dict) -> NodeData:
+ return d.get(('__node_data', None), {})
+
+
+def get_node_data_at_path(d: dict, tpath):
+ if not tpath:
+ return {}
+ # operates on actual paths, not names:
+ if not isinstance(tpath[0], tuple):
+ raise ValueError('must be path of tuples')
+ while tpath and d:
+ d = d.get(tpath[0], {})
+ tpath = tpath[1:]
+
+ return get_node_data(d)
+
+
+def node_data_difference(a: NodeData, b: NodeData):
+ out = ''
+ for fld in fields(NodeData):
+ if fld.name in ('children', 'files'):
+ continue
+ a_fld = getattr(a, fld.name)
+ b_fld = getattr(b, fld.name)
+ if a_fld != b_fld:
+ out += f'prev: {a.files[-1:]} {a.path} {fld.name}: {a_fld}\n'
+ out += f'new: {b.files[-1:]} {b.path} {fld.name}: {b_fld}\n'
+ out += '\n'
+
+ return out
+
+
+def collapse(d: OpData, acc: dict = None) -> tuple[dict, str, bool]:
+ err = False
+ inner_err = False
+ out = ''
+ inner_out = ''
+ if acc is None:
+ acc = {}
+ if not isinstance(d, dict):
+ return d
+ for k, v in d.items():
+ if isinstance(k, tuple):
+ name = key_name(k)
+ if name != '__node_data':
+ new_data = get_node_data(v)
+ if name in list(acc.keys()):
+ err = True
+ prev_data = acc[name].get('__node_data', {})
+ if prev_data:
+ out += f'prev: {prev_data["file"]} {prev_data["path"]}\n'
+ else:
+ out += '\n'
+ out += f'new: {new_data.file} {new_data.path}\n\n'
+ else:
+ new_data.children = list(map(lambda t: t[0], new_data.children))
+ acc[name] = {}
+ acc[name]['__node_data'] = asdict(new_data)
+ inner, o, e = collapse(v)
+ inner_err |= e
+ inner_out += o
+ acc[name].update(inner)
+ else:
+ name = k
+ acc[name] = v
+
+ err |= inner_err
+ out += inner_out
+
+ return acc, out, err
class OpXml:
def __init__(self):
self.op_ref = {}
- def define(self, op_ref: list[PathData]) -> None:
+ def define(self, op_ref: dict) -> None:
self.op_ref = op_ref
- def _get_op_ref_path(self, path: list[str]) -> list[PathData]:
- def _get_path_list(path: list[str], l: list[PathData]) -> list[PathData]:
- if not path:
- return l
- for d in l:
- if path[0] in list(d):
- return _get_path_list(path[1:], d[path[0]])
- return []
- l = self.op_ref
- return _get_path_list(path, l)
+ def walk(self, func):
+ def walk_op_data(obj, func):
+ if isinstance(obj, dict):
+ for k, v in obj.items():
+ if isinstance(k, tuple):
+ res = func(k, v)
+ yield res
+ yield from walk_op_data(v, func)
+
+ return walk_op_data(self.op_ref, func)
+
+ @staticmethod
+ def get_node_data_func(k, v):
+ if key_name(k) == '__node_data':
+ return v
+ return None
+
+ def walk_node_data(self):
+ return filterfalse(lambda x: x is None, self.walk(self.get_node_data_func))
+
+ def lookup(
+ self, path: list[str], tag_values: bool = False, last_node_type: str = ''
+ ) -> (OpData, list[str]):
+ path = path[:]
+
+ ref_path = []
+
+ def prune_tree(d: dict, p: list[str]):
+ p = p[:]
+ if not d or not isinstance(d, dict) or not p:
+ return d
+ op_data: dict = subdict_of_name(p[0], d)
+ op_keys = list(op_data.keys())
+ ref_path.append(p[0])
+ if len(p) < 2:
+ # check last node_type
+ if last_node_type:
+ keys = list(filter(lambda t: t[1] == last_node_type, op_keys))
+ values = list(map(lambda t: op_data[t], keys))
+ return dict(zip(keys, values))
+ return op_data
+
+ if p[1] not in key_names(next_keys(op_data)):
+ # check if tag_values
+ if tag_values:
+ p = p[2:]
+ keys = list(filter(is_tag_node, op_keys))
+ values = list(map(lambda t: prune_tree(op_data[t], p), keys))
+ return dict(zip(keys, values))
+ return {}
+
+ p = p[1:]
+ op_data = list(map(lambda t: prune_tree(op_data[t], p), op_keys))
+
+ return dict(zip(op_keys, op_data))
+
+ return prune_tree(self.op_ref, path), ref_path
+
+ def lookup_node_data(
+ self, path: list[str], tag_values: bool = False, last_node_type: str = ''
+ ) -> list[NodeData]:
+ res = []
+ d, ref_path = self.lookup(path, tag_values, last_node_type)
+ paths = list(tuple_paths(d))
+ paths = match_tuple_paths(ref_path, paths)
+ for p in paths:
+ res.append(get_node_data_at_path(d, p))
+
+ return res
diff --git a/python/vyos/xml_ref/update_cache.py b/python/vyos/xml_ref/update_cache.py
index 0842bcbe9..6643f9dc4 100755
--- a/python/vyos/xml_ref/update_cache.py
+++ b/python/vyos/xml_ref/update_cache.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as