summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/package-smoketest.yml1
-rw-r--r--.gitignore2
-rw-r--r--.gitmodules4
-rw-r--r--CODEOWNERS2
-rw-r--r--Makefile14
-rw-r--r--data/templates/frr/ldpd.frr.j210
-rw-r--r--data/templates/ipsec/charon_systemd.conf.j218
-rw-r--r--data/templates/rsyslog/rsyslog.conf.j211
-rw-r--r--debian/control3
-rw-r--r--interface-definitions/interfaces_virtual-ethernet.xml.in4
-rw-r--r--interface-definitions/protocols_mpls.xml.in24
-rw-r--r--interface-definitions/system_syslog.xml.in4
m---------libvyosconfig0
-rw-r--r--python/setup.py11
-rw-r--r--python/vyos/configtree.py29
-rw-r--r--python/vyos/frrender.py6
-rw-r--r--python/vyos/kea.py8
-rwxr-xr-xpython/vyos/proto/generate_dataclass.py178
-rw-r--r--python/vyos/proto/vyconf_client.py87
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py49
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_loopback.py5
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_virtual-ethernet.py5
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireless.py25
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_mpls.py71
-rwxr-xr-xsmoketest/scripts/cli/test_system_syslog.py4
-rwxr-xr-xsrc/conf_mode/pki.py12
-rwxr-xr-xsrc/conf_mode/system_login_banner.py8
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py2
-rwxr-xr-xsrc/op_mode/firewall.py113
-rwxr-xr-xsrc/op_mode/image_installer.py24
-rwxr-xr-xsrc/services/vyos-commitd2
31 files changed, 598 insertions, 138 deletions
diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml
index 2c90fed39..5ed764217 100644
--- a/.github/workflows/package-smoketest.yml
+++ b/.github/workflows/package-smoketest.yml
@@ -42,6 +42,7 @@ jobs:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
+ submodules: true
- name: Build vyos-1x package
run: |
cd packages/vyos-1x; dpkg-buildpackage -uc -us -tc -b
diff --git a/.gitignore b/.gitignore
index 27ed8000f..839d2afff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -153,6 +153,8 @@ data/configd-include.json
# autogenerated vyos-commitd protobuf files
python/vyos/proto/*pb2.py
+python/vyos/proto/*.desc
+python/vyos/proto/vyconf_proto.py
# We do not use pip
Pipfile
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..05eaf619f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "libvyosconfig"]
+ path = libvyosconfig
+ url = ../../vyos/libvyosconfig
+ branch = current
diff --git a/CODEOWNERS b/CODEOWNERS
index 72ddbde91..0bf2e6d79 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,2 +1,2 @@
# Users from reviewers github team
-* @vyos/reviewers
+# * @vyos/reviewers
diff --git a/Makefile b/Makefile
index 6b8827c84..763f44952 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,7 @@ BUILD_ARCH := $(shell dpkg-architecture -q DEB_BUILD_ARCH)
J2LINT := $(shell command -v j2lint 2> /dev/null)
PYLINT_FILES := $(shell git ls-files *.py src/migration-scripts)
LIBVYOSCONFIG_BUILD_PATH := /tmp/libvyosconfig/_build/libvyosconfig.so
+LIBVYOSCONFIG_STATUS := $(shell git submodule status)
config_xml_src = $(wildcard interface-definitions/*.xml.in)
config_xml_obj = $(config_xml_src:.xml.in=.xml)
@@ -23,12 +24,13 @@ op_xml_obj = $(op_xml_src:.xml.in=.xml)
.PHONY: libvyosconfig
.ONESHELL:
libvyosconfig:
- if ! [ -f $(LIBVYOSCONFIG_BUILD_PATH) ]; then
- rm -rf /tmp/libvyosconfig && \
- git clone https://github.com/vyos/libvyosconfig.git /tmp/libvyosconfig || exit 1
- cd /tmp/libvyosconfig && \
- git checkout 27e4b0a5eaf77d9a1f5e1f6dcaa109e5d73c51d1 || exit 1
- eval $$(opam env --root=/opt/opam --set-root) && ./build.sh
+ if test ! -f $(LIBVYOSCONFIG_BUILD_PATH); then
+ if ! echo $(firstword $(LIBVYOSCONFIG_STATUS))|grep -Eq '^[a-z0-9]'; then
+ git submodule sync; git submodule update --init --remote
+ fi
+ rm -rf /tmp/libvyosconfig && mkdir /tmp/libvyosconfig
+ cp -r libvyosconfig /tmp && cd /tmp/libvyosconfig && \
+ eval $$(opam env --root=/opt/opam --set-root) && ./build.sh || exit 1
fi
.PHONY: interface_definitions
diff --git a/data/templates/frr/ldpd.frr.j2 b/data/templates/frr/ldpd.frr.j2
index 9a893cc55..b8fb0cfc7 100644
--- a/data/templates/frr/ldpd.frr.j2
+++ b/data/templates/frr/ldpd.frr.j2
@@ -82,8 +82,11 @@ mpls ldp
{% endfor %}
{% endif %}
{% if ldp.interface is vyos_defined %}
-{% for interface in ldp.interface %}
+{% for interface, iface_config in ldp.interface.items() %}
interface {{ interface }}
+{% if iface_config.disable_establish_hello is vyos_defined %}
+ disable-establish-hello
+{% endif %}
exit
{% endfor %}
{% endif %}
@@ -135,8 +138,11 @@ mpls ldp
{% endfor %}
{% endif %}
{% if ldp.interface is vyos_defined %}
-{% for interface in ldp.interface %}
+{% for interface, iface_config in ldp.interface.items() %}
interface {{ interface }}
+{% if iface_config.disable_establish_hello is vyos_defined %}
+ disable-establish-hello
+{% endif %}
{% endfor %}
{% endif %}
exit-address-family
diff --git a/data/templates/ipsec/charon_systemd.conf.j2 b/data/templates/ipsec/charon_systemd.conf.j2
new file mode 100644
index 000000000..368aa1ae3
--- /dev/null
+++ b/data/templates/ipsec/charon_systemd.conf.j2
@@ -0,0 +1,18 @@
+# Generated by ${vyos_conf_scripts_dir}/vpn_ipsec.py
+
+charon-systemd {
+
+ # Section to configure native systemd journal logger, very similar to the
+ # syslog logger as described in LOGGER CONFIGURATION in strongswan.conf(5).
+ journal {
+
+ # Loglevel for a specific subsystem.
+ # <subsystem> = <default>
+
+{% if log.level is vyos_defined %}
+ # Default loglevel.
+ default = {{ log.level }}
+{% endif %}
+ }
+
+}
diff --git a/data/templates/rsyslog/rsyslog.conf.j2 b/data/templates/rsyslog/rsyslog.conf.j2
index 68e34f3f8..6ef2afcaf 100644
--- a/data/templates/rsyslog/rsyslog.conf.j2
+++ b/data/templates/rsyslog/rsyslog.conf.j2
@@ -1,16 +1,15 @@
### Autogenerated by system_syslog.py ###
#### MODULES ####
-# Load input modules for local logging and kernel logging
+# Load input modules for local logging and journald
# Old-style log file format with low-precision timestamps
# A modern-style logfile format with high-precision timestamps and timezone info
# RSYSLOG_FileFormat
module(load="builtin:omfile" Template="RSYSLOG_TraditionalFileFormat")
-module(load="imuxsock") # provides support for local system logging
-module(load="imklog") # provides kernel logging support
+module(load="imuxsock") # provides support for local system logging (collection from /dev/log unix socket)
-# Import logs from journald
+# Import logs from journald, which includes kernel log messages
module(
load="imjournal"
StateFile="/var/spool/rsyslog/imjournal.state" # Persistent state file to track the journal cursor
@@ -103,9 +102,9 @@ if prifilt("{{ tmp | join(',') }}") then {
port="{{ remote_options.port }}"
protocol="{{ remote_options.protocol }}"
{% if remote_options.format.include_timezone is vyos_defined %}
- template="SyslogProtocol23Format"
+ template="RSYSLOG_SyslogProtocol23Format"
{% endif %}
- TCP_Framing="{{ 'octed-counted' if remote_options.format.octet_counted is vyos_defined else 'traditional' }}"
+ TCP_Framing="{{ 'octet-counted' if remote_options.format.octet_counted is vyos_defined else 'traditional' }}"
{% if remote_options.source_address is vyos_defined %}
Address="{{ remote_options.source_address }}"
{% endif %}
diff --git a/debian/control b/debian/control
index 20b1a228c..4186dfb3b 100644
--- a/debian/control
+++ b/debian/control
@@ -42,7 +42,8 @@ Pre-Depends:
libnss-tacplus [amd64],
libpam-tacplus [amd64],
libpam-radius-auth (= 1.5.0-cl3u7) [amd64],
- libnss-mapuser (= 1.1.0-cl3u3) [amd64]
+ libnss-mapuser (= 1.1.0-cl3u3) [amd64],
+ tzdata (>= 2025b)
Depends:
## Fundamentals
${python3:Depends} (>= 3.10),
diff --git a/interface-definitions/interfaces_virtual-ethernet.xml.in b/interface-definitions/interfaces_virtual-ethernet.xml.in
index c4610feec..2dfbd50b8 100644
--- a/interface-definitions/interfaces_virtual-ethernet.xml.in
+++ b/interface-definitions/interfaces_virtual-ethernet.xml.in
@@ -21,6 +21,10 @@
#include <include/interface/dhcp-options.xml.i>
#include <include/interface/dhcpv6-options.xml.i>
#include <include/interface/disable.xml.i>
+ #include <include/interface/mtu-68-16000.xml.i>
+ <leafNode name="mtu">
+ <defaultValue>1500</defaultValue>
+ </leafNode>
#include <include/interface/netns.xml.i>
#include <include/interface/vif-s.xml.i>
#include <include/interface/vif.xml.i>
diff --git a/interface-definitions/protocols_mpls.xml.in b/interface-definitions/protocols_mpls.xml.in
index 831601fc6..fc1864f38 100644
--- a/interface-definitions/protocols_mpls.xml.in
+++ b/interface-definitions/protocols_mpls.xml.in
@@ -524,7 +524,29 @@
</node>
</children>
</node>
- #include <include/generic-interface-multi.xml.i>
+ <tagNode name="interface">
+ <properties>
+ <help>Interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <constraint>
+ #include <include/constraint/interface-name.xml.i>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="disable-establish-hello">
+ <properties>
+ <help>Disable response to hello packet with an additional hello LDP packet</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
</children>
</node>
<node name="parameters">
diff --git a/interface-definitions/system_syslog.xml.in b/interface-definitions/system_syslog.xml.in
index 8b2d9cab7..116cbde73 100644
--- a/interface-definitions/system_syslog.xml.in
+++ b/interface-definitions/system_syslog.xml.in
@@ -46,13 +46,13 @@
<children>
<leafNode name="octet-counted">
<properties>
- <help>Allows for the transmission of all characters inside a syslog message</help>
+ <help>Allows for the transmission of multi-line messages (TCP only)</help>
<valueless/>
</properties>
</leafNode>
<leafNode name="include-timezone">
<properties>
- <help>Include system timezone in syslog message</help>
+ <help>Use RFC 5424 format (with RFC 3339 timestamp and timezone)</help>
<valueless/>
</properties>
</leafNode>
diff --git a/libvyosconfig b/libvyosconfig
new file mode 160000
+Subproject 5cd5a6f5f63bb3cc68af04fe4a98059b43cef65
diff --git a/python/setup.py b/python/setup.py
index 96dc211f7..571b956ee 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -7,6 +7,9 @@ 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 [
_[0].replace('/','.')
@@ -37,9 +40,17 @@ class GenerateProto(build_py):
'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)
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index dade852c7..ff40fbad0 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -523,35 +523,6 @@ def mask_inclusive(left, right, libpath=LIBPATH):
return tree
-def show_commit_data(active_tree, proposed_tree, libpath=LIBPATH):
- if not (
- isinstance(active_tree, ConfigTree) and isinstance(proposed_tree, ConfigTree)
- ):
- raise TypeError('Arguments must be instances of ConfigTree')
-
- __lib = cdll.LoadLibrary(libpath)
- __show_commit_data = __lib.show_commit_data
- __show_commit_data.argtypes = [c_void_p, c_void_p]
- __show_commit_data.restype = c_char_p
-
- res = __show_commit_data(active_tree._get_config(), proposed_tree._get_config())
-
- return res.decode()
-
-
-def test_commit(active_tree, proposed_tree, libpath=LIBPATH):
- if not (
- isinstance(active_tree, ConfigTree) and isinstance(proposed_tree, ConfigTree)
- ):
- raise TypeError('Arguments must be instances of ConfigTree')
-
- __lib = cdll.LoadLibrary(libpath)
- __test_commit = __lib.test_commit
- __test_commit.argtypes = [c_void_p, c_void_p]
-
- __test_commit(active_tree._get_config(), proposed_tree._get_config())
-
-
def reference_tree_to_json(from_dir, to_file, internal_cache='', libpath=LIBPATH):
try:
__lib = cdll.LoadLibrary(libpath)
diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py
index ba44978d1..8d469e3e2 100644
--- a/python/vyos/frrender.py
+++ b/python/vyos/frrender.py
@@ -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 = {}
@@ -599,8 +603,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
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index c7947af3e..9fc5dde3d 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -483,10 +483,10 @@ def kea_get_domain_from_subnet_id(config, inet, subnet_id):
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']
+ # 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
diff --git a/python/vyos/proto/generate_dataclass.py b/python/vyos/proto/generate_dataclass.py
new file mode 100755
index 000000000..c6296c568
--- /dev/null
+++ b/python/vyos/proto/generate_dataclass.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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/vyconf_client.py b/python/vyos/proto/vyconf_client.py
new file mode 100644
index 000000000..f34549309
--- /dev/null
+++ b/python/vyos/proto/vyconf_client.py
@@ -0,0 +1,87 @@
+# Copyright 2025 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)
+
+ 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/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 80d200e97..3e2653a2f 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -46,6 +46,8 @@ dhclient_process_name = 'dhclient'
dhcp6c_base_dir = directories['dhcp6_client_dir']
dhcp6c_process_name = 'dhcp6c'
+MSG_TESTCASE_UNSUPPORTED = 'unsupported on interface family'
+
server_ca_root_cert_data = """
MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw
HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa
@@ -136,6 +138,7 @@ def is_mirrored_to(interface, mirror_if, qdisc):
if mirror_if in tmp:
ret_val = True
return ret_val
+
class BasicInterfaceTest:
class TestCase(VyOSUnitTestSHIM.TestCase):
_test_dhcp = False
@@ -219,7 +222,7 @@ class BasicInterfaceTest:
def test_dhcp_disable_interface(self):
if not self._test_dhcp:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
# When interface is configured as admin down, it must be admin down
# even when dhcpc starts on the given interface
@@ -242,7 +245,7 @@ class BasicInterfaceTest:
def test_dhcp_client_options(self):
if not self._test_dhcp or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
client_id = 'VyOS-router'
distance = '100'
@@ -282,7 +285,7 @@ class BasicInterfaceTest:
def test_dhcp_vrf(self):
if not self._test_dhcp or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
cli_default_metric = default_value(self._base_path + [self._interfaces[0],
'dhcp-options', 'default-route-distance'])
@@ -339,7 +342,7 @@ class BasicInterfaceTest:
def test_dhcpv6_vrf(self):
if not self._test_ipv6_dhcpc6 or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
vrf_name = 'purple6'
self.cli_set(['vrf', 'name', vrf_name, 'table', '65001'])
@@ -391,7 +394,7 @@ class BasicInterfaceTest:
def test_move_interface_between_vrf_instances(self):
if not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
vrf1_name = 'smoketest_mgmt1'
vrf1_table = '5424'
@@ -436,7 +439,7 @@ class BasicInterfaceTest:
def test_add_to_invalid_vrf(self):
if not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
# move interface into first VRF
for interface in self._interfaces:
@@ -454,7 +457,7 @@ class BasicInterfaceTest:
def test_span_mirror(self):
if not self._mirror_interfaces:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
# Check the two-way mirror rules of ingress and egress
for mirror in self._mirror_interfaces:
@@ -563,7 +566,7 @@ class BasicInterfaceTest:
def test_ipv6_link_local_address(self):
# Common function for IPv6 link-local address assignemnts
if not self._test_ipv6:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -594,7 +597,7 @@ class BasicInterfaceTest:
def test_interface_mtu(self):
if not self._test_mtu:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for intf in self._interfaces:
base = self._base_path + [intf]
@@ -613,8 +616,8 @@ class BasicInterfaceTest:
def test_mtu_1200_no_ipv6_interface(self):
# Testcase if MTU can be changed to 1200 on non IPv6
# enabled interfaces
- if not self._test_mtu:
- self.skipTest('not supported')
+ if not self._test_mtu or not self._test_ipv6:
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
old_mtu = self._mtu
self._mtu = '1200'
@@ -650,7 +653,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_vlan:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -695,7 +698,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_vlan or not self._test_mtu:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
mtu_1500 = '1500'
mtu_9000 = '9000'
@@ -741,7 +744,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_vlan:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -811,7 +814,7 @@ class BasicInterfaceTest:
def test_vif_8021q_lower_up_down(self):
# Testcase for https://vyos.dev/T3349
if not self._test_vlan:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -851,7 +854,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_qinq:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -918,7 +921,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_qinq:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -956,7 +959,7 @@ class BasicInterfaceTest:
def test_interface_ip_options(self):
if not self._test_ip:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
arp_tmo = '300'
mss = '1420'
@@ -1058,7 +1061,7 @@ class BasicInterfaceTest:
def test_interface_ipv6_options(self):
if not self._test_ipv6:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
mss = '1400'
dad_transmits = '10'
@@ -1119,7 +1122,7 @@ class BasicInterfaceTest:
def test_dhcpv6_client_options(self):
if not self._test_ipv6_dhcpc6:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
duid_base = 10
for interface in self._interfaces:
@@ -1170,7 +1173,7 @@ class BasicInterfaceTest:
def test_dhcpv6pd_auto_sla_id(self):
if not self._test_ipv6_pd:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
prefix_len = '56'
sla_len = str(64 - int(prefix_len))
@@ -1231,7 +1234,7 @@ class BasicInterfaceTest:
def test_dhcpv6pd_manual_sla_id(self):
if not self._test_ipv6_pd:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
prefix_len = '56'
sla_len = str(64 - int(prefix_len))
@@ -1297,7 +1300,7 @@ class BasicInterfaceTest:
def test_eapol(self):
if not self._test_eapol:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
cfg_dir = '/run/wpa_supplicant'
diff --git a/smoketest/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py
index 0454dc658..f4b6038c5 100755
--- a/smoketest/scripts/cli/test_interfaces_loopback.py
+++ b/smoketest/scripts/cli/test_interfaces_loopback.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2023 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 VyOS maintainers and contributors
#
# 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
@@ -17,6 +17,7 @@
import unittest
from base_interfaces_test import BasicInterfaceTest
+from base_interfaces_test import MSG_TESTCASE_UNSUPPORTED
from netifaces import interfaces
from vyos.utils.network import is_intf_addr_assigned
@@ -53,7 +54,7 @@ class LoopbackInterfaceTest(BasicInterfaceTest.TestCase):
self.assertTrue(is_intf_addr_assigned('lo', addr))
def test_interface_disable(self):
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py
index c6a4613a7..b2af86139 100755
--- a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright (C) 2023-2025 VyOS maintainers and contributors
#
# 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
@@ -34,9 +34,6 @@ class VEthInterfaceTest(BasicInterfaceTest.TestCase):
# call base-classes classmethod
super(VEthInterfaceTest, cls).setUpClass()
- def test_vif_8021q_mtu_limits(self):
- self.skipTest('not supported')
-
# As we always need a pair of veth interfaces, we can not rely on the base
# class check to determine if there is a dhcp6c or dhclient instance running.
# This test will always fail as there is an instance running on the peer
diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py
index b8b18f30f..1c69c1be5 100755
--- a/smoketest/scripts/cli/test_interfaces_wireless.py
+++ b/smoketest/scripts/cli/test_interfaces_wireless.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2024 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 VyOS maintainers and contributors
#
# 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
@@ -64,13 +64,23 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
# call base-classes classmethod
super(WirelessInterfaceTest, cls).setUpClass()
- # T5245 - currently testcases are disabled
- cls._test_ipv6 = False
- cls._test_vlan = False
+ # If any wireless interface is based on mac80211_hwsim, disable all
+ # VLAN related testcases. See T5245, T7325
+ tmp = read_file('/proc/modules')
+ if 'mac80211_hwsim' in tmp:
+ cls._test_ipv6 = False
+ cls._test_vlan = False
+ cls._test_qinq = False
+
+ # Loading mac80211_hwsim module created two WIFI Interfaces in the
+ # background (wlan0 and wlan1), remove them to have a clean test start.
+ # This must happen AFTER the above check for unsupported drivers
+ for interface in cls._interfaces:
+ if interface_exists(interface):
+ call(f'sudo iw dev {interface} del')
cls.cli_set(cls, wifi_cc_path + [country])
-
def test_wireless_add_single_ip_address(self):
# derived method to check if member interfaces are enslaved properly
super().test_add_single_ip_address()
@@ -627,9 +637,4 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
if __name__ == '__main__':
check_kmod('mac80211_hwsim')
- # loading the module created two WIFI Interfaces in the background (wlan0 and wlan1)
- # remove them to have a clean test start
- for interface in ['wlan0', 'wlan1']:
- if interface_exists(interface):
- call(f'sudo iw dev {interface} del')
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py
index 654f2f099..3840c24f4 100755
--- a/smoketest/scripts/cli/test_protocols_mpls.py
+++ b/smoketest/scripts/cli/test_protocols_mpls.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 VyOS maintainers and contributors
#
# 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
@@ -121,5 +121,74 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
for interface in interfaces:
self.assertIn(f' interface {interface}', afiv4_config)
+ def test_02_mpls_disable_establish_hello(self):
+ router_id = '1.2.3.4'
+ transport_ipv4_addr = '5.6.7.8'
+ transport_ipv6_addr = '2001:db8:1111::1111'
+ interfaces = Section.interfaces('ethernet')
+
+ self.cli_set(base_path + ['router-id', router_id])
+
+ # At least one LDP interface must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'disable-establish-hello'])
+
+ # LDP transport address missing
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['discovery', 'transport-ipv4-address', transport_ipv4_addr])
+ self.cli_set(base_path + ['discovery', 'transport-ipv6-address', transport_ipv6_addr])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Validate configuration
+ frrconfig = self.getFRRconfig('mpls ldp', endsection='^exit')
+ self.assertIn(f'mpls ldp', frrconfig)
+ self.assertIn(f' router-id {router_id}', frrconfig)
+
+ # Validate AFI IPv4
+ afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit',
+ substring=' address-family ipv4',
+ endsubsection='^ exit-address-family')
+ self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config)
+ for interface in interfaces:
+ self.assertIn(f' interface {interface}', afiv4_config)
+ self.assertIn(f' disable-establish-hello', afiv4_config)
+
+ # Validate AFI IPv6
+ afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit',
+ substring=' address-family ipv6',
+ endsubsection='^ exit-address-family')
+ self.assertIn(f' discovery transport-address {transport_ipv6_addr}', afiv6_config)
+ for interface in interfaces:
+ self.assertIn(f' interface {interface}', afiv6_config)
+ self.assertIn(f' disable-establish-hello', afiv6_config)
+
+ # Delete disable-establish-hello
+ for interface in interfaces:
+ self.cli_delete(base_path + ['interface', interface, 'disable-establish-hello'])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Validate AFI IPv4
+ afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit',
+ substring=' address-family ipv4',
+ endsubsection='^ exit-address-family')
+ # Validate AFI IPv6
+ afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit',
+ substring=' address-family ipv6',
+ endsubsection='^ exit-address-family')
+ # Check deleted 'disable-establish-hello' option per interface
+ for interface in interfaces:
+ self.assertIn(f' interface {interface}', afiv4_config)
+ self.assertNotIn(f' disable-establish-hello', afiv4_config)
+ self.assertIn(f' interface {interface}', afiv6_config)
+ self.assertNotIn(f' disable-establish-hello', afiv6_config)
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py
index ba325ced8..6eae3f19d 100755
--- a/smoketest/scripts/cli/test_system_syslog.py
+++ b/smoketest/scripts/cli/test_system_syslog.py
@@ -223,10 +223,10 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase):
if 'format' in remote_options:
if 'include-timezone' in remote_options['format']:
- self.assertIn( ' template="SyslogProtocol23Format"', config)
+ self.assertIn( ' template="RSYSLOG_SyslogProtocol23Format"', config)
if 'octet-counted' in remote_options['format']:
- self.assertIn( ' TCP_Framing="octed-counted"', config)
+ self.assertIn( ' TCP_Framing="octet-counted"', config)
else:
self.assertIn( ' TCP_Framing="traditional"', config)
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index acea2c9be..724f97555 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -440,13 +440,21 @@ def generate(pki):
for name, cert_conf in pki['certificate'].items():
if 'acme' in cert_conf:
certbot_list.append(name)
- # generate certificate if not found on disk
+ # There is no ACME/certbot managed certificate presend on the
+ # system, generate it
if name not in certbot_list_on_disk:
certbot_request(name, cert_conf['acme'], dry_run=False)
+ # Now that the certificate was properly generated we have
+ # the PEM files on disk. We need to add the certificate to
+ # certbot_list_on_disk to automatically import the CA chain
+ certbot_list_on_disk.append(name)
+ # We alredy had an ACME managed certificate on the system, but
+ # something changed in the configuration
elif changed_certificates != None and name in changed_certificates:
- # when something for the certificate changed, we should delete it
+ # Delete old ACME certificate first
if name in certbot_list_on_disk:
certbot_delete(name)
+ # Request new certificate via certbot
certbot_request(name, cert_conf['acme'], dry_run=False)
# Cleanup certbot configuration and certificates if no longer in use by CLI
diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py
index 5826d8042..cdd066649 100755
--- a/src/conf_mode/system_login_banner.py
+++ b/src/conf_mode/system_login_banner.py
@@ -95,8 +95,12 @@ def apply(banner):
render(POSTLOGIN_FILE, 'login/default_motd.j2', banner,
permission=0o644, user='root', group='root')
- render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', banner,
- permission=0o644, user='root', group='root')
+ if banner['version_data']['build_type'] != 'release':
+ render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2',
+ banner,
+ permission=0o644,
+ user='root',
+ group='root')
return None
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 71a503e61..2754314f7 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -64,6 +64,7 @@ swanctl_dir = '/etc/swanctl'
charon_conf = '/etc/strongswan.d/charon.conf'
charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf'
+charon_systemd_conf = '/etc/strongswan.d/charon-systemd.conf'
interface_conf = '/etc/strongswan.d/interfaces_use.conf'
swanctl_conf = f'{swanctl_dir}/swanctl.conf'
@@ -745,6 +746,7 @@ def generate(ipsec):
render(charon_conf, 'ipsec/charon.j2', ipsec)
render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec)
render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec)
+ render(charon_systemd_conf, 'ipsec/charon_systemd.conf.j2', ipsec)
render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec)
render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec)
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 7a3ab921d..086536e4e 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -148,6 +148,38 @@ def get_nftables_group_members(family, table, name):
return out
+def get_nftables_remote_group_members(family, table, name):
+ prefix = 'ip6' if family == 'ipv6' else 'ip'
+ out = []
+
+ try:
+ results_str = cmd(f'nft -j list set {prefix} {table} {name}')
+ results = json.loads(results_str)
+ except:
+ return out
+
+ if 'nftables' not in results:
+ return out
+
+ for obj in results['nftables']:
+ if 'set' not in obj:
+ continue
+
+ set_obj = obj['set']
+ if 'elem' in set_obj:
+ for elem in set_obj['elem']:
+ # search for single IP elements
+ if isinstance(elem, str):
+ out.append(elem)
+ # search for prefix elements
+ elif isinstance(elem, dict) and 'prefix' in elem:
+ out.append(f"{elem['prefix']['addr']}/{elem['prefix']['len']}")
+ # search for IP range elements
+ elif isinstance(elem, dict) and 'range' in elem:
+ out.append(f"{elem['range'][0]}-{elem['range'][1]}")
+
+ return out
+
def output_firewall_vertical(rules, headers, adjust=True):
for rule in rules:
adjusted_rule = rule + [""] * (len(headers) - len(rule)) if adjust else rule # account for different header length, like default-action
@@ -556,32 +588,8 @@ def show_firewall_group(name=None):
header_tail = []
for group_type, group_type_conf in firewall['group'].items():
- ##
- if group_type != 'dynamic_group':
-
- for group_name, group_conf in group_type_conf.items():
- if name and name != group_name:
- continue
-
- references = find_references(group_type, group_name)
- row = [group_name, textwrap.fill(group_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D']
- if 'address' in group_conf:
- row.append("\n".join(sorted(group_conf['address'])))
- elif 'network' in group_conf:
- row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
- elif 'mac_address' in group_conf:
- row.append("\n".join(sorted(group_conf['mac_address'])))
- elif 'port' in group_conf:
- row.append("\n".join(sorted(group_conf['port'])))
- elif 'interface' in group_conf:
- row.append("\n".join(sorted(group_conf['interface'])))
- elif 'url' in group_conf:
- row.append(group_conf['url'])
- else:
- row.append('N/D')
- rows.append(row)
-
- else:
+ # interate over dynamic-groups
+ if group_type == 'dynamic_group':
if not args.detail:
header_tail = ['Timeout', 'Expires']
@@ -628,6 +636,59 @@ def show_firewall_group(name=None):
header_tail += [""] * (len(members) - 1)
rows.append(row)
+ # iterate over remote-groups
+ elif group_type == 'remote_group':
+ for remote_name, remote_conf in group_type_conf.items():
+ if name and name != remote_name:
+ continue
+
+ references = find_references(group_type, remote_name)
+ row = [remote_name, textwrap.fill(remote_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D']
+ members = get_nftables_remote_group_members("ipv4", 'vyos_filter', f'R_{remote_name}')
+
+ if 'url' in remote_conf:
+ # display only the url if no members are found for both views
+ if not members:
+ if args.detail:
+ header_tail = ['Remote URL']
+ row.append('N/D')
+ row.append(remote_conf['url'])
+ else:
+ row.append(remote_conf['url'])
+ rows.append(row)
+ else:
+ # display all table elements in detail view
+ if args.detail:
+ header_tail = ['Remote URL']
+ row += [' '.join(members)]
+ row.append(remote_conf['url'])
+ rows.append(row)
+ else:
+ row.append(remote_conf['url'])
+ rows.append(row)
+
+ # catch the rest of the group types
+ else:
+ for group_name, group_conf in group_type_conf.items():
+ if name and name != group_name:
+ continue
+
+ references = find_references(group_type, group_name)
+ row = [group_name, textwrap.fill(group_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D']
+ if 'address' in group_conf:
+ row.append("\n".join(sorted(group_conf['address'])))
+ elif 'network' in group_conf:
+ row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
+ elif 'mac_address' in group_conf:
+ row.append("\n".join(sorted(group_conf['mac_address'])))
+ elif 'port' in group_conf:
+ row.append("\n".join(sorted(group_conf['port'])))
+ elif 'interface' in group_conf:
+ row.append("\n".join(sorted(group_conf['interface'])))
+ else:
+ row.append('N/D')
+ rows.append(row)
+
if rows:
print('Firewall Groups\n')
if args.detail:
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index 82756daec..9c17d0229 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -58,6 +58,7 @@ MSG_ERR_FLAVOR_MISMATCH: str = 'The current image flavor is "{0}", the new image
MSG_ERR_MISSING_ARCHITECTURE: str = 'The new image version data does not specify architecture, cannot check compatibility (is it a legacy release image?)'
MSG_ERR_MISSING_FLAVOR: str = 'The new image version data does not specify flavor, cannot check compatibility (is it a legacy release image?)'
MSG_ERR_CORRUPT_CURRENT_IMAGE: str = 'Version data in the current image is malformed: missing flavor and/or architecture fields. Upgrade compatibility cannot be checked.'
+MSG_ERR_UNSUPPORTED_SIGNATURE_TYPE: str = 'Unsupported signature type, signature cannot be verified.'
MSG_INFO_INSTALL_WELCOME: str = 'Welcome to VyOS installation!\nThis command will install VyOS to your permanent storage.'
MSG_INFO_INSTALL_EXIT: str = 'Exiting from VyOS installation'
MSG_INFO_INSTALL_SUCCESS: str = 'The image installed successfully; please reboot now.'
@@ -514,7 +515,6 @@ def validate_signature(file_path: str, sign_type: str) -> None:
"""
print('Validating signature')
signature_valid: bool = False
- # validate with minisig
if sign_type == 'minisig':
pub_key_list = glob('/usr/share/vyos/keys/*.minisign.pub')
for pubkey in pub_key_list:
@@ -523,11 +523,8 @@ def validate_signature(file_path: str, sign_type: str) -> None:
signature_valid = True
break
Path(f'{file_path}.minisig').unlink()
- # validate with GPG
- if sign_type == 'asc':
- if run(f'gpg --verify ${file_path}.asc ${file_path}') == 0:
- signature_valid = True
- Path(f'{file_path}.asc').unlink()
+ else:
+ exit(MSG_ERR_UNSUPPORTED_SIGNATURE_TYPE)
# warn or pass
if not signature_valid:
@@ -581,15 +578,18 @@ def image_fetch(image_path: str, vrf: str = None,
try:
# check a type of path
if urlparse(image_path).scheme:
- # download an image
+ # Download the image file
ISO_DOWNLOAD_PATH = os.path.join(os.path.expanduser("~"), '{0}.iso'.format(uuid4()))
download_file(ISO_DOWNLOAD_PATH, image_path, vrf,
username, password,
progressbar=True, check_space=True)
- # download a signature
+ # Download the image signature
+ # VyOS only supports minisign signatures at the moment,
+ # but we keep the logic for multiple signatures
+ # in case we add something new in the future
sign_file = (False, '')
- for sign_type in ['minisig', 'asc']:
+ for sign_type in ['minisig']:
try:
download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}',
f'{image_path}.{sign_type}', vrf,
@@ -597,8 +597,8 @@ def image_fetch(image_path: str, vrf: str = None,
sign_file = (True, sign_type)
break
except Exception:
- print(f'{sign_type} signature is not available')
- # validate a signature if it is available
+ print(f'Could not download {sign_type} signature')
+ # Validate the signature if it is available
if sign_file[0]:
validate_signature(ISO_DOWNLOAD_PATH, sign_file[1])
else:
@@ -1007,7 +1007,7 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
Path(target_config_dir).mkdir(parents=True)
chown(target_config_dir, group='vyattacfg')
chmod_2775(target_config_dir)
- copytree('/opt/vyatta/etc/config/', target_config_dir,
+ copytree('/opt/vyatta/etc/config/', target_config_dir, symlinks=True,
copy_function=copy_preserve_owner, dirs_exist_ok=True)
else:
Path(target_config_dir).mkdir(parents=True)
diff --git a/src/services/vyos-commitd b/src/services/vyos-commitd
index 8dbd39058..55f0c8741 100755
--- a/src/services/vyos-commitd
+++ b/src/services/vyos-commitd
@@ -72,8 +72,6 @@ class Session:
# pylint: disable=too-many-instance-attributes
session_id: str = ''
- named_active: str = None
- named_proposed: str = None
dry_run: bool = False
atomic: bool = False
background: bool = False