diff options
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 @@ -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 |
