From c5d987717237d001ae0cddd5c11109ab1f72882c Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Mon, 23 Jun 2025 20:08:37 +0200
Subject: [PATCH 01/17] T7557: python3-nose -> python3-nose2
---
Makefile | 2 +-
debian/control | 2 +-
nose2.cfg | 9 +++++++++
python/vyos/airbag.py | 4 +++-
test-requirements.txt | 2 +-
5 files changed, 15 insertions(+), 4 deletions(-)
create mode 100644 nose2.cfg
diff --git a/Makefile b/Makefile
index 244f465186d..15b6b8aebf8 100644
--- a/Makefile
+++ b/Makefile
@@ -107,7 +107,7 @@ clean:
.PHONY: test
test: generate-configd-include-json
set -e; python3 -m compileall -q -x '/vmware-tools/scripts/' .
- PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose
+ PYTHONPATH=python/ python3 -m nose2 -v
.PHONY: check_migration_scripts_executable
.ONESHELL:
diff --git a/debian/control b/debian/control
index f5853ba2c61..b8279116c73 100644
--- a/debian/control
+++ b/debian/control
@@ -31,7 +31,7 @@ Build-Depends:
python3-hurry.filesize,
python3-netaddr,
python3-netifaces,
- python3-nose,
+ python3-nose2,
python3-jinja2,
python3-paramiko,
python3-passlib,
diff --git a/nose2.cfg b/nose2.cfg
new file mode 100644
index 00000000000..e2fbd610a1d
--- /dev/null
+++ b/nose2.cfg
@@ -0,0 +1,9 @@
+[unittest]
+start-dir = src
+code-directories = conf_mode
+ op_mode
+ completion
+ validators
+ tests
+test-file-pattern = test_*.py
+test-method-prefix = test
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
index 1dcccdd4769..1d00b6367e0 100644
--- a/python/vyos/airbag.py
+++ b/python/vyos/airbag.py
@@ -24,6 +24,8 @@
def enable(log=True):
+ if 'nose2' in sys.modules:
+ return
if log:
_intercepting_logger()
_intercepting_exceptions()
@@ -157,7 +159,7 @@ def _intercepting_exceptions(_singleton=[False]):
{instructions}
When reporting problems, please include as much information as possible:
-- do not obfuscate any data (feel free to contact us privately if your
+- do not obfuscate any data (feel free to contact us privately if your
business policy requires it)
- and include all the information presented below
diff --git a/test-requirements.txt b/test-requirements.txt
index a475e0a1672..10f52300eef 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,6 +1,6 @@
python/
lxml
pylint
-nose
+nose2
coverage
jinja2
From 3f4be8ec60e39bc6f77e167970503402ad89494a Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Tue, 24 Jun 2025 15:48:12 +0200
Subject: [PATCH 02/17] T7557: Check serial sessions without utmp
---
python/vyos/utils/serial.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/python/vyos/utils/serial.py b/python/vyos/utils/serial.py
index 36b483e9196..64e63e3ab92 100644
--- a/python/vyos/utils/serial.py
+++ b/python/vyos/utils/serial.py
@@ -24,7 +24,6 @@
RE_GETTY_DEVICES = re.compile(r'.+@(.+).service$')
SD_UNIT_PATH = '/run/systemd/system'
-UTMP_PATH = '/run/utmp'
def get_serial_units(include_devices=[]):
# Since we cannot depend on the current config for decommissioned ports,
@@ -60,10 +59,10 @@ def get_authenticated_ports(units):
#
# We can safely skip blank or LOGIN sessions with valid device names.
#
- for line in cmd(f'utmpdump {UTMP_PATH}').splitlines():
- row = line.split('] [')
- user_name = row[3].strip()
- user_term = row[4].strip()
+ for line in cmd(f'who').splitlines():
+ row = line.split()
+ user_name = row[0].strip()
+ user_term = row[1].strip()
if user_name and user_name != 'LOGIN' and user_term in ports:
connected.append(user_term)
From 110be2f716a2cdab11cab37457c4a983dc2728ee Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Tue, 24 Jun 2025 16:22:56 +0200
Subject: [PATCH 03/17] T7557: Use update-locale to change time format
---
src/conf_mode/system_option.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py
index fbe7231df06..2c5b12903f1 100755
--- a/src/conf_mode/system_option.py
+++ b/src/conf_mode/system_option.py
@@ -251,7 +251,7 @@ def apply(options):
# Time format 12|24-hour
if 'time_format' in options:
time_format = time_format_to_locale.get(options['time_format'])
- cmd(f'localectl set-locale LC_TIME={time_format}')
+ cmd(f'update-locale LC_TIME={time_format}')
# Reload UDEV, required for USB auto suspend
cmd('udevadm control --reload-rules')
From c3ff111564b7db3bfe12e36162fd1beba12cefed Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Tue, 24 Jun 2025 18:09:26 +0200
Subject: [PATCH 04/17] T7557: Add e2fsprogs package for installer
---
debian/control | 1 +
1 file changed, 1 insertion(+)
diff --git a/debian/control b/debian/control
index b8279116c73..b59956e57f3 100644
--- a/debian/control
+++ b/debian/control
@@ -132,6 +132,7 @@ Depends:
mokutil,
shim-signed [amd64],
sbsigntool [amd64],
+ e2fsprogs,
# Image signature verification tool
minisign,
# Live filesystem tools
From 59665ece6c30056fad9a3a46595565f8cbe33ca1 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Wed, 25 Jun 2025 12:20:07 +0200
Subject: [PATCH 05/17] T7557: Update stunnel4 systemd unit
---
src/conf_mode/service_stunnel.py | 4 ++--
src/systemd/{stunnel.service => stunnel4.service} | 0
2 files changed, 2 insertions(+), 2 deletions(-)
rename src/systemd/{stunnel.service => stunnel4.service} (100%)
diff --git a/src/conf_mode/service_stunnel.py b/src/conf_mode/service_stunnel.py
index 5ea5b88b4a6..2781c5ceaf2 100644
--- a/src/conf_mode/service_stunnel.py
+++ b/src/conf_mode/service_stunnel.py
@@ -248,9 +248,9 @@ def generate(stunnel):
def apply(stunnel):
if not stunnel or ('client' not in stunnel and 'server' not in stunnel):
- call('systemctl stop stunnel.service')
+ call('systemctl stop stunnel4.service')
else:
- call('systemctl restart stunnel.service')
+ call('systemctl restart stunnel4.service')
if __name__ == '__main__':
diff --git a/src/systemd/stunnel.service b/src/systemd/stunnel4.service
similarity index 100%
rename from src/systemd/stunnel.service
rename to src/systemd/stunnel4.service
From 657e552af3511305cd25650afc7cb1bb4ce3a75e Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Wed, 25 Jun 2025 12:29:19 +0200
Subject: [PATCH 06/17] T7557: Add systemd units for sysv init scripts
---
src/systemd/igmpproxy.service | 18 ++++++++++++++++++
src/systemd/qat_service.service | 18 ++++++++++++++++++
src/systemd/wide-dhcpv6-client.service | 18 ++++++++++++++++++
3 files changed, 54 insertions(+)
create mode 100644 src/systemd/igmpproxy.service
create mode 100644 src/systemd/qat_service.service
create mode 100644 src/systemd/wide-dhcpv6-client.service
diff --git a/src/systemd/igmpproxy.service b/src/systemd/igmpproxy.service
new file mode 100644
index 00000000000..2c4de1ed09c
--- /dev/null
+++ b/src/systemd/igmpproxy.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=IGMP Proxy
+After=vyos-router.service
+
+[Service]
+Type=forking
+Restart=no
+TimeoutSec=5min
+IgnoreSIGPIPE=no
+KillMode=process
+GuessMainPID=no
+RemainAfterExit=yes
+SuccessExitStatus=5 6
+ExecStart=/etc/init.d/igmpproxy start
+ExecStop=/etc/init.d/igmpproxy stop
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/qat_service.service b/src/systemd/qat_service.service
new file mode 100644
index 00000000000..e62a476a057
--- /dev/null
+++ b/src/systemd/qat_service.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=Intel QAT service
+After=vyos-router.service
+
+[Service]
+Type=forking
+Restart=no
+TimeoutSec=5min
+IgnoreSIGPIPE=no
+KillMode=process
+GuessMainPID=no
+RemainAfterExit=yes
+SuccessExitStatus=5 6
+ExecStart=/etc/init.d/qat_service start
+ExecStop=/etc/init.d/qat_service stop
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/wide-dhcpv6-client.service b/src/systemd/wide-dhcpv6-client.service
new file mode 100644
index 00000000000..69b005c06fd
--- /dev/null
+++ b/src/systemd/wide-dhcpv6-client.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=WIDE DHCPv6 Client
+After=vyos-router.service
+
+[Service]
+Type=forking
+Restart=no
+TimeoutSec=5min
+IgnoreSIGPIPE=no
+KillMode=process
+GuessMainPID=no
+RemainAfterExit=yes
+SuccessExitStatus=5 6
+ExecStart=/etc/init.d/wide-dhcpv6-client start
+ExecStop=/etc/init.d/wide-dhcpv6-client stop
+
+[Install]
+WantedBy=multi-user.target
From b1542a23ec2e25c59614ffa8b3ed11ab20b618fa Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Wed, 25 Jun 2025 14:25:04 +0200
Subject: [PATCH 07/17] T7557: Set dmesg tty loglevel to warning
---
src/init/vyos-router | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/init/vyos-router b/src/init/vyos-router
index 7a02ce5641a..3be946d431c 100755
--- a/src/init/vyos-router
+++ b/src/init/vyos-router
@@ -453,6 +453,9 @@ start ()
{
log_success_msg "Starting VyOS router"
+ # Set dmesg tty loglevel to warning
+ dmesg -n 4
+
# reset and clean config files
security_reset || log_failure_msg "security reset failed"
From f84a04638f8252625481d5c104de41ca220d4b61 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Thu, 26 Jun 2025 15:20:39 +0200
Subject: [PATCH 08/17] T7557: DSA keys not supported in Trixie
---
data/templates/ssh/sshd_config.j2 | 1 -
.../include/version/ssh-version.xml.i | 2 +-
.../include/version/system-version.xml.i | 2 +-
interface-definitions/service_ssh.xml.in | 8 ++--
interface-definitions/system_login.xml.in | 8 +---
python/vyos/utils/auth.py | 4 +-
smoketest/config-tests/basic-vyos | 2 +
smoketest/configs/basic-vyos | 4 ++
smoketest/scripts/cli/test_service_ssh.py | 3 --
src/conf_mode/service_ssh.py | 4 --
src/migration-scripts/ssh/2-to-3 | 46 +++++++++++++++++++
src/migration-scripts/system/29-to-30 | 34 ++++++++++++++
12 files changed, 96 insertions(+), 22 deletions(-)
create mode 100644 src/migration-scripts/ssh/2-to-3
create mode 100644 src/migration-scripts/system/29-to-30
diff --git a/data/templates/ssh/sshd_config.j2 b/data/templates/ssh/sshd_config.j2
index 1315bf2cbea..6d29468d245 100644
--- a/data/templates/ssh/sshd_config.j2
+++ b/data/templates/ssh/sshd_config.j2
@@ -7,7 +7,6 @@
#
Protocol 2
HostKey /etc/ssh/ssh_host_rsa_key
-HostKey /etc/ssh/ssh_host_dsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
SyslogFacility AUTH
diff --git a/interface-definitions/include/version/ssh-version.xml.i b/interface-definitions/include/version/ssh-version.xml.i
index 0f25caf98ff..05cf431a799 100644
--- a/interface-definitions/include/version/ssh-version.xml.i
+++ b/interface-definitions/include/version/ssh-version.xml.i
@@ -1,3 +1,3 @@
-
+
diff --git a/interface-definitions/include/version/system-version.xml.i b/interface-definitions/include/version/system-version.xml.i
index 5cdece74a95..a9987e8e44e 100644
--- a/interface-definitions/include/version/system-version.xml.i
+++ b/interface-definitions/include/version/system-version.xml.i
@@ -1,3 +1,3 @@
-
+
diff --git a/interface-definitions/service_ssh.xml.in b/interface-definitions/service_ssh.xml.in
index c659a7db7e7..f85c5a5e321 100644
--- a/interface-definitions/service_ssh.xml.in
+++ b/interface-definitions/service_ssh.xml.in
@@ -138,11 +138,11 @@
Allowed host key signature algorithms
- ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512 ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 sk-ecdsa-sha2-nistp256@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-rsa-cert-v01@openssh.com rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512-cert-v01@openssh.com ssh-dss-cert-v01@openssh.com ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com
+ ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512 ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 sk-ecdsa-sha2-nistp256@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-rsa-cert-v01@openssh.com rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512-cert-v01@openssh.com ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com
- (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ssh-rsa|rsa-sha2-256|rsa-sha2-512|ssh-dss|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ecdsa-sha2-nistp256@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512-cert-v01@openssh.com|ssh-dss-cert-v01@openssh.com|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com)
+ (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ssh-rsa|rsa-sha2-256|rsa-sha2-512|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ecdsa-sha2-nistp256@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512-cert-v01@openssh.com|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com)
@@ -151,11 +151,11 @@
Allowed pubkey signature algorithms
- ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384 ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521 ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-dss ssh-dss-cert-v01@openssh.com ssh-rsa ssh-rsa-cert-v01@openssh.com rsa-sha2-256 rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512 rsa-sha2-512-cert-v01@openssh.com
+ ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ecdsa-sha2-nistp256 ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384 ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521 ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-rsa ssh-rsa-cert-v01@openssh.com rsa-sha2-256 rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512 rsa-sha2-512-cert-v01@openssh.com
- (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ecdsa-sha2-nistp256|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-dss|ssh-dss-cert-v01@openssh.com|ssh-rsa|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512|rsa-sha2-512-cert-v01@openssh.com)
+ (ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ecdsa-sha2-nistp256|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-rsa|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512|rsa-sha2-512-cert-v01@openssh.com)
diff --git a/interface-definitions/system_login.xml.in b/interface-definitions/system_login.xml.in
index a13ba10ea1f..268f5c67e46 100644
--- a/interface-definitions/system_login.xml.in
+++ b/interface-definitions/system_login.xml.in
@@ -138,12 +138,8 @@
SSH public key type
- ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ecdsa-sha2-nistp256@openssh.com sk-ssh-ed25519@openssh.com
+ ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ecdsa-sha2-nistp256@openssh.com sk-ssh-ed25519@openssh.com
-
- ssh-dss
- Digital Signature Algorithm (DSA) key support
-
ssh-rsa
Key pair based on RSA algorithm
@@ -173,7 +169,7 @@
Elliptic curve 25519 security key
- (ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519|sk-ecdsa-sha2-nistp256@openssh.com|sk-ssh-ed25519@openssh.com)
+ (ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519|sk-ecdsa-sha2-nistp256@openssh.com|sk-ssh-ed25519@openssh.com)
diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py
index 6e816af71b1..d29dd432988 100644
--- a/python/vyos/utils/auth.py
+++ b/python/vyos/utils/auth.py
@@ -104,8 +104,8 @@ def split_ssh_public_key(key_string, defaultname=""):
else:
key_type, key_data, key_name = parts[0], parts[1], defaultname
- if key_type not in ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']:
- raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type))
+ if key_type not in ['ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']:
+ raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type))
return({"type": key_type, "data": key_data, "name": key_name})
diff --git a/smoketest/config-tests/basic-vyos b/smoketest/config-tests/basic-vyos
index aaf450e80c1..b62dbf2d867 100644
--- a/smoketest/config-tests/basic-vyos
+++ b/smoketest/config-tests/basic-vyos
@@ -86,12 +86,14 @@ set service ssh ciphers 'aes192-ctr'
set service ssh ciphers 'aes256-ctr'
set service ssh ciphers 'chacha20-poly1305@openssh.com'
set service ssh ciphers 'rijndael-cbc@lysator.liu.se'
+set service ssh hostkey-algorithm 'ssh-rsa'
set service ssh key-exchange 'curve25519-sha256@libssh.org'
set service ssh key-exchange 'diffie-hellman-group1-sha1'
set service ssh key-exchange 'diffie-hellman-group-exchange-sha1'
set service ssh key-exchange 'diffie-hellman-group-exchange-sha256'
set service ssh listen-address '192.168.0.1'
set service ssh port '22'
+set service ssh pubkey-accepted-algorithm 'ssh-rsa'
set system config-management commit-revisions '100'
set system conntrack ignore ipv4 rule 1 destination address '192.0.2.2'
set system conntrack ignore ipv4 rule 1 source address '192.0.2.1'
diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos
index 5f7a71237cb..3bdf5d5b749 100644
--- a/smoketest/configs/basic-vyos
+++ b/smoketest/configs/basic-vyos
@@ -228,6 +228,10 @@ service {
key-exchange curve25519-sha256@libssh.org
key-exchange diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group-exchange-sha256
port 22
+ hostkey-algorithm ssh-rsa
+ hostkey-algorithm ssh-dss
+ pubkey-accepted-algorithm ssh-rsa
+ pubkey-accepted-algorithm ssh-dss
}
}
system {
diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py
index 32bc3cc8937..7b92458f712 100755
--- a/smoketest/scripts/cli/test_service_ssh.py
+++ b/smoketest/scripts/cli/test_service_ssh.py
@@ -38,7 +38,6 @@
pki_path = ['pki']
key_rsa = '/etc/ssh/ssh_host_rsa_key'
-key_dsa = '/etc/ssh/ssh_host_dsa_key'
key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
trusted_user_ca = config_files['sshd_user_ca']
test_command = 'uname -a'
@@ -156,7 +155,6 @@ def tearDown(self):
self.cli_commit()
self.assertTrue(os.path.isfile(key_rsa))
- self.assertTrue(os.path.isfile(key_dsa))
self.assertTrue(os.path.isfile(key_ed25519))
# Established SSH connections remains running after service is stopped.
@@ -408,7 +406,6 @@ def test_ssh_pubkey_accepted_algorithm(self):
'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp384',
'ecdsa-sha2-nistp521',
- 'ssh-dss',
'ssh-rsa',
'rsa-sha2-256',
'rsa-sha2-512',
diff --git a/src/conf_mode/service_ssh.py b/src/conf_mode/service_ssh.py
index bf8afe8b77e..22adac72506 100755
--- a/src/conf_mode/service_ssh.py
+++ b/src/conf_mode/service_ssh.py
@@ -42,7 +42,6 @@
sshguard_whitelist = '/etc/sshguard/whitelist'
key_rsa = '/etc/ssh/ssh_host_rsa_key'
-key_dsa = '/etc/ssh/ssh_host_dsa_key'
key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
trusted_user_ca = config_files['sshd_user_ca']
@@ -116,9 +115,6 @@ def generate(ssh):
if not os.path.isfile(key_rsa):
syslog(LOG_INFO, 'SSH RSA host key not found, generating new key!')
call(f'ssh-keygen -q -N "" -t rsa -f {key_rsa}')
- if not os.path.isfile(key_dsa):
- syslog(LOG_INFO, 'SSH DSA host key not found, generating new key!')
- call(f'ssh-keygen -q -N "" -t dsa -f {key_dsa}')
if not os.path.isfile(key_ed25519):
syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!')
call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}')
diff --git a/src/migration-scripts/ssh/2-to-3 b/src/migration-scripts/ssh/2-to-3
new file mode 100644
index 00000000000..0e3cf4da864
--- /dev/null
+++ b/src/migration-scripts/ssh/2-to-3
@@ -0,0 +1,46 @@
+# Copyright VyOS maintainers and contributors
+#
+# 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 .
+
+# T7557: OpenSSH on Trixie does not support DSS keys
+# https://www.debian.org/releases/trixie/release-notes/issues.en.html#openssh-no-longer-supports-dsa-keys
+
+
+from vyos.configtree import ConfigTree
+
+base = ['service', 'ssh']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base):
+ # Nothing to do
+ return
+
+ dss_algo = [
+ 'ssh-dss',
+ 'ssh-dss-cert-v01@openssh.com',
+ ]
+
+ path_hostkey = base + ['hostkey-algorithm']
+ if config.exists(path_hostkey):
+ values = config.return_values(path_hostkey)
+ for algo in dss_algo:
+ if algo in values:
+ config.delete_value(path_hostkey, algo)
+
+ path_pubkey = base + ['pubkey-accepted-algorithm']
+ if config.exists(path_pubkey):
+ values = config.return_values(path_pubkey)
+ for algo in dss_algo:
+ if algo in values:
+ config.delete_value(path_pubkey, algo)
diff --git a/src/migration-scripts/system/29-to-30 b/src/migration-scripts/system/29-to-30
new file mode 100644
index 00000000000..a5c2d0f3cbc
--- /dev/null
+++ b/src/migration-scripts/system/29-to-30
@@ -0,0 +1,34 @@
+# Copyright VyOS maintainers and contributors
+#
+# 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 .
+
+# T7557: OpenSSH does not support DSA keys in Debian Trixie
+
+from vyos.configtree import ConfigTree
+
+base = ['system', 'login', 'user']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base):
+ return
+
+ for user in config.list_nodes(base):
+ key_base = base + [user, 'authentication', 'public-keys']
+ if config.exists(key_base):
+ for key in config.list_nodes(key_base):
+ key_type = key_base + [key, 'type']
+ if config.exists(key_type):
+ tmp = config.return_value(key_type)
+ if tmp == 'ssh-dss':
+ config.delete(key_base + [key])
From 5dab88456ba65135bef8ea7247e5d3c4b8b70487 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Fri, 27 Jun 2025 00:22:39 +0200
Subject: [PATCH 09/17] T7557: Use systemd quadlet for containers
---
data/templates/container/quadlet-network.j2 | 8 +
data/templates/container/quadlet-unit.j2 | 11 +
data/templates/container/systemd-unit.j2 | 17 --
smoketest/scripts/cli/test_container.py | 23 +-
src/conf_mode/container.py | 261 +++++++++-----------
5 files changed, 150 insertions(+), 170 deletions(-)
create mode 100644 data/templates/container/quadlet-network.j2
create mode 100644 data/templates/container/quadlet-unit.j2
delete mode 100644 data/templates/container/systemd-unit.j2
diff --git a/data/templates/container/quadlet-network.j2 b/data/templates/container/quadlet-network.j2
new file mode 100644
index 00000000000..f253be557da
--- /dev/null
+++ b/data/templates/container/quadlet-network.j2
@@ -0,0 +1,8 @@
+### Autogenerated by container.py ###
+[Unit]
+Description=VyOS Network {{ name }}
+
+[Network]
+{% for opt in opts %}
+{{ opt }}
+{% endfor %}
diff --git a/data/templates/container/quadlet-unit.j2 b/data/templates/container/quadlet-unit.j2
new file mode 100644
index 00000000000..7546e3f1f82
--- /dev/null
+++ b/data/templates/container/quadlet-unit.j2
@@ -0,0 +1,11 @@
+### Autogenerated by container.py ###
+[Unit]
+Description=VyOS Container {{ name }}
+
+[Container]
+{% for opt in opts %}
+{{ opt }}
+{% endfor %}
+
+[Service]
+Restart={{ restart }}
diff --git a/data/templates/container/systemd-unit.j2 b/data/templates/container/systemd-unit.j2
deleted file mode 100644
index d379f0a0765..00000000000
--- a/data/templates/container/systemd-unit.j2
+++ /dev/null
@@ -1,17 +0,0 @@
-### Autogenerated by container.py ###
-[Unit]
-Description=VyOS Container {{ name }}
-
-[Service]
-Environment=PODMAN_SYSTEMD_UNIT=%n
-Restart=on-failure
-ExecStartPre=/bin/rm -f %t/%n.pid %t/%n.cid
-ExecStart=/usr/bin/podman run \
- --conmon-pidfile %t/%n.pid --cidfile %t/%n.cid --cgroups=no-conmon \
- {{ run_args }}
-ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n.cid -t 5
-ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n.cid
-ExecStopPost=/bin/rm -f %t/%n.cid
-PIDFile=%t/%n.pid
-KillMode=control-group
-Type=forking
diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py
index 7590ed1cdef..5e265a39022 100755
--- a/smoketest/scripts/cli/test_container.py
+++ b/smoketest/scripts/cli/test_container.py
@@ -28,7 +28,6 @@
base_path = ['container']
PROCESS_NAME = 'conmon'
-PROCESS_PIDFILE = '/run/vyos-container-{0}.service.pid'
busybox_image = 'busybox:stable'
busybox_image_path = '/usr/share/vyos/busybox-stable.tar'
@@ -68,9 +67,13 @@ def tearDown(self):
self.assertIsNone(process_named_running(PROCESS_NAME))
# Ensure systemd units are removed
- units = glob.glob('/run/systemd/system/vyos-container-*')
+ units = glob.glob('/run/containers/systemd/vyos*')
self.assertEqual(units, [])
+ def is_running(self, name):
+ command = f'systemctl show vyos-{name} --property=ActiveState --value'
+ return cmd(command).strip() == 'active'
+
def test_basic(self):
cont_name = 'c1'
@@ -99,12 +102,7 @@ def test_basic(self):
# commit changes
self.cli_commit()
- pid = 0
- with open(PROCESS_PIDFILE.format(cont_name), 'r') as f:
- pid = int(f.read())
-
- # Check for running process
- self.assertEqual(process_named_running(PROCESS_NAME), pid)
+ self.assertTrue(self.is_running(cont_name))
# verify
tmp = cmd(f'sudo podman exec -it {cont_name} sysctl kernel.msgmax')
@@ -143,6 +141,8 @@ def test_name_server(self):
self.cli_set(base_path + ['network', net_name, 'no-name-server'])
self.cli_commit()
+ self.assertTrue(self.is_running(cont_name))
+
n = cmd_to_json(f'sudo podman inspect {cont_name}')
self.assertEqual(n['HostConfig']['Dns'][0], name_server)
@@ -158,12 +158,7 @@ def test_cpu_limit(self):
self.cli_commit()
- pid = 0
- with open(PROCESS_PIDFILE.format(cont_name), 'r') as f:
- pid = int(f.read())
-
- # Check for running process
- self.assertEqual(process_named_running(PROCESS_NAME), pid)
+ self.assertTrue(self.is_running(cont_name))
def test_ipv4_network(self):
prefix = '192.0.2.0/24'
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 4ec9b8849ba..c0473d96b5e 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -50,7 +50,7 @@
config_containers = '/etc/containers/containers.conf'
config_registry = '/etc/containers/registries.conf'
config_storage = '/etc/containers/storage.conf'
-systemd_unit_path = '/run/systemd/system'
+quadlet_unit_path = '/run/containers/systemd'
def _cmd(command):
@@ -303,60 +303,106 @@ def verify(container):
return None
+def generate_network_options(name, network_config):
+ out = [
+ f'NetworkName={name}',
+ 'Driver=bridge',
+ 'Internal=false',
+ 'IPAMDriver=host-local',
+ f'PodmanArgs=--interface-name=pod-{name}',
+ ]
+
+ # TODO: if 'no_name_server' in network_config:
+ # DNS appears broken in trixie
+ out.append('DisableDNS=true')
+
+ mtu = network_config['mtu'] if 'mtu' in network_config else '1500'
+ out.append(f'Options=mtu={mtu}')
+
+ ipv6 = False
+
+ for prefix in network_config['prefix']:
+ gw = inc_ip(prefix, 1)
+ out.append(f'Subnet={prefix}')
+ out.append(f'Gateway={gw}')
+
+ if is_ipv6(prefix):
+ ipv6 = True
+
+ if ipv6:
+ out.append('IPv6=true')
+
+ return out
+
+def generate_quadlet_options(name, container_config):
+ out = [
+ f'ContainerName={name}',
+ f'Image={container_config['image']}',
+ f'LogDriver={container_config['log_driver']}',
+ f'Memory={container_config['memory']}m',
+ f'ShmSize={container_config['shared_memory']}m',
+ f'PodmanArgs=--cpus={container_config['cpu_quota']}',
+ 'PodmanArgs=--no-healthcheck',
+ 'PodmanArgs=--interactive',
+ 'PodmanArgs=--tty',
+ ]
-def generate_run_arguments(name, container_config):
- image = container_config['image']
- cpu_quota = container_config['cpu_quota']
- memory = container_config['memory']
- shared_memory = container_config['shared_memory']
- restart = container_config['restart']
- log_driver = container_config['log_driver']
+ if 'allow_host_networks' in container_config:
+ out.append('Network=host')
- # Add sysctl options
- sysctl_opt = ''
- if 'sysctl' in container_config and 'parameter' in container_config['sysctl']:
- for k, v in container_config['sysctl']['parameter'].items():
- sysctl_opt += f" --sysctl \"{k}={v['value']}\""
+ if 'allow_host_pid' in container_config:
+ out.append('PodmanArgs=--pid host')
- # Add capability options. Should be in uppercase
- capabilities = ''
if 'capability' in container_config:
for cap in container_config['capability']:
cap = cap.upper().replace('-', '_')
- capabilities += f' --cap-add={cap}'
+ out.append(f'AddCapability={cap}')
- # Grant root capabilities to the container
- privileged = ''
- if 'privileged' in container_config:
- privileged = '--privileged'
+ if 'command' in container_config:
+ command = container_config['command'].strip()
+
+ if 'arguments' in container_config:
+ command += ' ' + container_config['arguments'].strip()
+
+ out.append(f'Exec={command}')
- # Add a host device to the container /dev/x:/dev/x
- device = ''
if 'device' in container_config:
for dev, dev_config in container_config['device'].items():
source_dev = dev_config['source']
dest_dev = dev_config['destination']
- device += f' --device={source_dev}:{dest_dev}'
+ out.append(f'AddDevice={source_dev}:{dest_dev}')
+
+ if 'entrypoint' in container_config:
+ entrypoint = container_config['entrypoint']
+ out.append(f'Entrypoint={entrypoint}')
- # Check/set environment options "-e foo=bar"
- env_opt = ''
if 'environment' in container_config:
for k, v in container_config['environment'].items():
- env_opt += f" --env \"{k}={v['value']}\""
+ out.append(f'Environment={k}={v}')
+
+ if 'host_name' in container_config:
+ hostname = container_config['host_name']
+ out.append(f'HostName={hostname}')
- # Check/set label options "--label foo=bar"
- label = ''
if 'label' in container_config:
for k, v in container_config['label'].items():
- label += f" --label \"{k}={v['value']}\""
+ out.append(f'Label={k}={v['value']}')
- hostname = ''
- if 'host_name' in container_config:
- hostname = container_config['host_name']
- hostname = f'--hostname {hostname}'
+ if 'name_server' in container_config:
+ for ns in container_config['name_server']:
+ out.append(f'DNS={ns}')
+
+ if 'network' in container_config:
+ for network in container_config['network']:
+ out.append(f'Network=vyos-{network}.network')
+
+ if 'address' in container_config['network'][network]:
+ for address in container_config['network'][network]['address']:
+ if is_ipv6(address):
+ out.append(f'IP6={address}')
+ else:
+ out.append(f'IP={address}')
- # Publish ports
- port = ''
if 'port' in container_config:
protocol = ''
for portmap in container_config['port']:
@@ -368,80 +414,39 @@ def generate_run_arguments(name, container_config):
# If listen_addresses is not empty, include them in the publish command
if listen_addresses:
for listen_address in listen_addresses:
- port += f' --publish {bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}'
+ out.append(f'PublishPort={bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}')
else:
# If listen_addresses is empty, just include the standard publish command
- port += f' --publish {sport}:{dport}/{protocol}'
+ out.append(f'PublishPort={sport}:{dport}/{protocol}')
+
+ if 'privileged' in container_config:
+ out.append('PodmanArgs=--privileged')
+
+ if 'sysctl' in container_config and 'parameter' in container_config['sysctl']:
+ for k, v in container_config['sysctl']['parameter'].items():
+ out.append(f'Sysctl={k}={v['value']}')
+
+ if 'tmpfs' in container_config:
+ for tmpfs_config in container_config['tmpfs'].values():
+ dest = tmpfs_config['destination']
+ size = tmpfs_config['size']
+ out.append(f'Mount=type=tmpfs,tmpfs-size={size}M,destination={dest}')
- # Set uid and gid
- uid = ''
if 'uid' in container_config:
uid = container_config['uid']
if 'gid' in container_config:
uid += ':' + container_config['gid']
- uid = f'--user {uid}'
+ out.append(f'User={uid}')
- # Bind volume
- volume = ''
if 'volume' in container_config:
- for vol, vol_config in container_config['volume'].items():
+ for _, vol_config in container_config['volume'].items():
svol = vol_config['source']
dvol = vol_config['destination']
mode = vol_config['mode']
prop = vol_config['propagation']
- volume += f' --volume {svol}:{dvol}:{mode},{prop}'
-
- # Mount tmpfs
- tmpfs = ''
- if 'tmpfs' in container_config:
- for tmpfs_config in container_config['tmpfs'].values():
- dest = tmpfs_config['destination']
- size = tmpfs_config['size']
- tmpfs += f' --mount=type=tmpfs,tmpfs-size={size}M,destination={dest}'
-
- host_pid = ''
- if 'allow_host_pid' in container_config:
- host_pid = '--pid host'
-
- name_server = ''
- if 'name_server' in container_config:
- for ns in container_config['name_server']:
- name_server += f'--dns {ns}'
-
- container_base_cmd = f'--detach --interactive --tty --replace {capabilities} {privileged} --cpus {cpu_quota} {sysctl_opt} ' \
- f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} --log-driver={log_driver} ' \
- f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid}'
-
- entrypoint = ''
- if 'entrypoint' in container_config:
- # it needs to be json-formatted with single quote on the outside
- entrypoint = json_write(container_config['entrypoint'].split()).replace('"', """)
- entrypoint = f'--entrypoint '{entrypoint}''
-
- command = ''
- if 'command' in container_config:
- command = container_config['command'].strip()
-
- command_arguments = ''
- if 'arguments' in container_config:
- command_arguments = container_config['arguments'].strip()
-
- if 'allow_host_networks' in container_config:
- return f'{container_base_cmd} --net host {entrypoint} {image} {command} {command_arguments}'.strip()
-
- ip_param = ''
- networks = ",".join(container_config['network'])
- for network in container_config['network']:
- if 'address' not in container_config['network'][network]:
- continue
- for address in container_config['network'][network]['address']:
- if is_ipv6(address):
- ip_param += f' --ip6 {address}'
- else:
- ip_param += f' --ip {address}'
-
- return f'{container_base_cmd} --no-healthcheck --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip()
+ out.append(f'Volume={svol}:{dvol}:{mode},{prop}')
+ return out
def generate(container):
# bail out early - looks like removal from running config
@@ -451,53 +456,25 @@ def generate(container):
os.unlink(file)
return None
- if 'network' in container:
- for network, network_config in container['network'].items():
- tmp = {
- 'name': network,
- 'id': sha256(f'{network}'.encode()).hexdigest(),
- 'driver': 'bridge',
- 'network_interface': f'pod-{network}',
- 'subnets': [],
- 'ipv6_enabled': False,
- 'internal': False,
- 'dns_enabled': True,
- 'ipam_options': {
- 'driver': 'host-local'
- },
- 'options': {
- 'mtu': '1500'
- }
- }
-
- if 'no_name_server' in network_config:
- tmp['dns_enabled'] = False
-
- if 'mtu' in network_config:
- tmp['options']['mtu'] = network_config['mtu']
-
- for prefix in network_config['prefix']:
- net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)}
- tmp['subnets'].append(net)
-
- if is_ipv6(prefix):
- tmp['ipv6_enabled'] = True
-
- write_file(f'/etc/containers/networks/{network}.json', json_write(tmp, indent=2))
-
render(config_containers, 'container/containers.conf.j2', container)
render(config_registry, 'container/registries.conf.j2', container)
render(config_storage, 'container/storage.conf.j2', container)
+ if 'network' in container:
+ for network, network_config in container['network'].items():
+ file_path = os.path.join(quadlet_unit_path, f'vyos-{network}.network')
+ opts = generate_network_options(network, network_config)
+ render(file_path, 'container/quadlet-network.j2', {'name': network, 'opts': opts})
+
if 'name' in container:
for name, container_config in container['name'].items():
if 'disable' in container_config:
continue
- file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
- run_args = generate_run_arguments(name, container_config)
- render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args, },
- formater=lambda _: _.replace(""", '"').replace("'", "'"))
+ file_path = os.path.join(quadlet_unit_path, f'vyos-{name}.container')
+ quadlet_opts = generate_quadlet_options(name, container_config)
+ restart = container_config['restart']
+ render(file_path, 'container/quadlet-unit.j2', {'name': name, 'opts': quadlet_opts, 'restart': restart})
return None
@@ -507,8 +484,8 @@ def apply(container):
# Option "--force" allows to delete containers with any status
if 'container_remove' in container:
for name in container['container_remove']:
- file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
- call(f'systemctl stop vyos-container-{name}.service')
+ file_path = os.path.join(quadlet_unit_path, f'vyos-{name}.container')
+ call(f'systemctl stop vyos-{name}')
if os.path.exists(file_path):
os.unlink(file_path)
@@ -517,7 +494,13 @@ def apply(container):
# Delete old networks if needed
if 'network_remove' in container:
for network in container['network_remove']:
- call(f'podman network rm {network} >/dev/null 2>&1')
+ file_path = os.path.join(quadlet_unit_path, f'vyos-{network}.network')
+ call(f'systemctl stop vyos-{network}-network')
+ call(f'podman network rm {network}')
+ if os.path.exists(file_path):
+ os.unlink(file_path)
+
+ call('systemctl daemon-reload')
# Add container
disabled_new = False
@@ -534,15 +517,15 @@ def apply(container):
# check if there is a container by that name running
tmp = _cmd('podman ps -a --format "{{.Names}}"')
if name in tmp:
- file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
- call(f'systemctl stop vyos-container-{name}.service')
+ file_path = os.path.join(quadlet_unit_path, f'vyos-{name}.container')
+ call(f'systemctl stop vyos-{name}')
if os.path.exists(file_path):
disabled_new = True
os.unlink(file_path)
continue
if 'container_restart' in container and name in container['container_restart']:
- cmd(f'systemctl restart vyos-container-{name}.service')
+ cmd(f'systemctl restart vyos-{name}')
if disabled_new:
call('systemctl daemon-reload')
From c67ae6c6e3b978c19024b4a84cac1ec8e45cea8a Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sat, 28 Jun 2025 22:54:10 +0200
Subject: [PATCH 10/17] T7557: Fix dns-forwarding, pending yaml re-write
---
data/templates/dns-forwarding/override.conf.j2 | 2 +-
data/templates/dns-forwarding/recursor.conf.lua.j2 | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/data/templates/dns-forwarding/override.conf.j2 b/data/templates/dns-forwarding/override.conf.j2
index 9d81a297796..a559da4f448 100644
--- a/data/templates/dns-forwarding/override.conf.j2
+++ b/data/templates/dns-forwarding/override.conf.j2
@@ -5,4 +5,4 @@ After=vyos-router.service
[Service]
RuntimeDirectoryPreserve=yes
ExecStart=
-ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir={{ config_dir }}
+ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir={{ config_dir }} --enable-old-settings
diff --git a/data/templates/dns-forwarding/recursor.conf.lua.j2 b/data/templates/dns-forwarding/recursor.conf.lua.j2
index 622283ad85a..48c9edbeaac 100644
--- a/data/templates/dns-forwarding/recursor.conf.lua.j2
+++ b/data/templates/dns-forwarding/recursor.conf.lua.j2
@@ -2,7 +2,9 @@
-- Do not edit, your changes will get overwritten --
-- Load DNSSEC root keys from dns-root-data package.
-dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua")
+-- dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua")
+-- lua-config no longer present, copying line from file
+readTrustAnchorsFromFile("/usr/share/dns/root.key")
-- Load lua from vyos-hostsd --
dofile("{{ config_dir }}/recursor.vyos-hostsd.conf.lua")
From 0fc8fb2b03749df766ac5d10881011af73534629 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 29 Jun 2025 22:28:15 +0200
Subject: [PATCH 11/17] T7557: Add python systemd module for interface
smoketests
---
debian/control | 1 +
1 file changed, 1 insertion(+)
diff --git a/debian/control b/debian/control
index b59956e57f3..d4f717d6db2 100644
--- a/debian/control
+++ b/debian/control
@@ -405,6 +405,7 @@ Description: VyOS configuration scripts and data for AWS Gateway Load Balancer
Package: vyos-1x-smoketest
Architecture: all
Depends:
+ python3-systemd,
skopeo,
snmp,
vyos-1x
From 056958fb6fd8829577f8d6ba65af5091a2cd025b Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 29 Jun 2025 22:28:47 +0200
Subject: [PATCH 12/17] T7557: Use systemd check for salt-minion process
---
smoketest/scripts/cli/test_service_salt-minion.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/smoketest/scripts/cli/test_service_salt-minion.py b/smoketest/scripts/cli/test_service_salt-minion.py
index 1ad1333f069..786d5fcb009 100755
--- a/smoketest/scripts/cli/test_service_salt-minion.py
+++ b/smoketest/scripts/cli/test_service_salt-minion.py
@@ -19,7 +19,7 @@
from socket import gethostname
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.utils.process import process_named_running
+from vyos.utils.process import is_systemd_service_active
from vyos.utils.file import read_file
from vyos.utils.process import cmd
@@ -47,7 +47,7 @@ def tearDownClass(cls):
def tearDown(self):
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.assertTrue(is_systemd_service_active(PROCESS_NAME))
# delete testing SALT config
self.cli_delete(base_path)
@@ -57,7 +57,7 @@ def tearDown(self):
# from the CI) salt-minion process is not killed by systemd. Apparently
# no issue on VMWare.
if cmd('systemd-detect-virt') != 'kvm':
- self.assertFalse(process_named_running(PROCESS_NAME))
+ self.assertFalse(is_systemd_service_active(PROCESS_NAME))
def test_default(self):
servers = ['192.0.2.1', '192.0.2.2']
From 4ee0bce6d261669a0c28b184cabb2a6d3a3d85d7 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Mon, 30 Jun 2025 00:53:41 +0200
Subject: [PATCH 13/17] T7557: Fix netplug hook for interface flapping
---
python/vyos/utils/network.py | 8 ++++++++
src/etc/netplug/vyos-netplug-dhcp-client | 4 +++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 0e2cc58cfbe..4c3c095df28 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -663,3 +663,11 @@ def is_valid_ipv6_address_or_range(addr: str) -> bool:
return ip_network(addr).version == 6
except:
return False
+
+def is_carrier_up(ifname: str) -> bool | None:
+ try:
+ carrier_path = f'/sys/class/net/{ifname}/carrier'
+ with open(carrier_path, 'r') as f:
+ return f.read().strip() == '1'
+ except:
+ return None
diff --git a/src/etc/netplug/vyos-netplug-dhcp-client b/src/etc/netplug/vyos-netplug-dhcp-client
index 31a0cc6a725..57b5813c126 100755
--- a/src/etc/netplug/vyos-netplug-dhcp-client
+++ b/src/etc/netplug/vyos-netplug-dhcp-client
@@ -22,6 +22,7 @@ from time import sleep
from vyos.config import Config
from vyos.ifconfig import Section
from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.network import is_carrier_up
from vyos.utils.process import cmd
from vyos.utils.process import is_systemd_service_active
from vyos.utils.commit import commit_in_progress
@@ -49,7 +50,8 @@ interface_path = ['interfaces'] + Section.get_config_path(interface).split()
systemdV4_service = f'dhclient@{interface}.service'
systemdV6_service = f'dhcp6c@{interface}.service'
-if in_out == 'out':
+# T7557 - Check the interface is still down at time of script
+if in_out == 'out' and not is_carrier_up(interface):
# Interface moved state to down
if is_systemd_service_active(systemdV4_service):
cmd(f'systemctl stop {systemdV4_service}')
From 8693365bec1e178a180906c3587ef338ad285817 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Mon, 30 Jun 2025 11:06:04 +0200
Subject: [PATCH 14/17] T7557: Fix bridge bpdu/root guard test
---
smoketest/scripts/cli/test_interfaces_bridge.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py
index 8502e6b2d2e..9bf5e2757ad 100755
--- a/smoketest/scripts/cli/test_interfaces_bridge.py
+++ b/smoketest/scripts/cli/test_interfaces_bridge.py
@@ -515,19 +515,16 @@ def test_bridge_root_bpdu_guard(self):
self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'root-guard'])
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_discard()
# Test if bpdu_guard configured
- self.cli_set(['interfaces', 'bridge', 'br0', 'stp'])
- self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'bpdu-guard'])
+ self.cli_delete(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'root-guard'])
self.cli_commit()
tmp = read_file(f'/sys/class/net/eth0/brport/bpdu_guard')
self.assertEqual(tmp, '1')
# Test if root_guard configured
- self.cli_delete(['interfaces', 'bridge', 'br0'])
- self.cli_set(['interfaces', 'bridge', 'br0', 'stp'])
+ self.cli_delete(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'bpdu-guard'])
self.cli_set(['interfaces', 'bridge', 'br0', 'member', 'interface', 'eth0', 'root-guard'])
self.cli_commit()
From ede2bf3f6cc6df069ce1cf76e939336d3246db94 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Mon, 30 Jun 2025 15:56:50 +0200
Subject: [PATCH 15/17] T7557: Prevent zombie process crashing pppoe smoketest
---
.../scripts/cli/test_interfaces_pppoe.py | 24 ++++++++++++-------
1 file changed, 15 insertions(+), 9 deletions(-)
diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py
index dbc70ebd4ba..834f1e9d7cc 100755
--- a/smoketest/scripts/cli/test_interfaces_pppoe.py
+++ b/smoketest/scripts/cli/test_interfaces_pppoe.py
@@ -15,8 +15,8 @@
# along with this program. If not, see .
import unittest
+import psutil
-from psutil import process_iter
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
@@ -48,10 +48,13 @@ def tearDown(self):
# Validate PPPoE client process
for interface in self._interfaces:
running = False
- for proc in process_iter():
- if interface in proc.cmdline():
- running = True
- break
+ for proc in psutil.process_iter():
+ try:
+ if interface in proc.cmdline():
+ running = True
+ break
+ except psutil.ZombieProcess:
+ pass
self.assertTrue(running)
self.cli_delete(base_path)
@@ -110,10 +113,13 @@ def test_pppoe_client_disabled_interface(self):
# Validate PPPoE client process - must not run as interfaces are disabled
for interface in self._interfaces:
running = False
- for proc in process_iter():
- if interface in proc.cmdline():
- running = True
- break
+ for proc in psutil.process_iter():
+ try:
+ if interface in proc.cmdline():
+ running = True
+ break
+ except psutil.ZombieProcess:
+ pass
self.assertFalse(running)
# enable PPPoE interfaces
From a8931b1be46e8ec77b76744bf42a3c08ef02af70 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 3 Aug 2025 22:37:22 +0200
Subject: [PATCH 16/17] T7557: Fix pylint detected errors
---
python/vyos/ifconfig/interface.py | 2 +-
src/conf_mode/firewall.py | 1 +
src/conf_mode/interfaces_wireless.py | 2 +-
src/conf_mode/protocols_bgp.py | 3 +-
src/conf_mode/system_task-scheduler.py | 6 +-
src/conf_mode/vpn_ipsec.py | 3 +
src/migration-scripts/ipoe-server/1-to-2 | 1 +
src/op_mode/conntrack_sync.py | 2 +
src/op_mode/dhcp.py | 1 +
src/op_mode/firewall.py | 75 ++++++++++++------------
src/op_mode/ikev2_profile_generator.py | 1 +
src/op_mode/nat.py | 6 ++
src/op_mode/otp.py | 1 +
src/op_mode/show_openconnect_otp.py | 1 +
src/op_mode/show_wwan.py | 2 +-
15 files changed, 62 insertions(+), 45 deletions(-)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 3b3536301d1..39ddc914b5e 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1629,7 +1629,7 @@ def set_mirror_redirect(self):
if direction == 'ingress':
handle = 'ffff: ingress'
parent = 'ffff:'
- elif direction == 'egress':
+ else: # egress
handle = '1: root prio'
parent = '1:'
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 90fdded998d..7c47bf8d59d 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -589,6 +589,7 @@ def parse_firewall_error(output):
# Parse the comment
parsed_entries = comment.split('-')
family = 'bridge' if parsed_entries[0] == 'bri' else parsed_entries[0]
+ chain = ''
if parsed_entries[1] == 'NAM':
chain = 'name'
elif parsed_entries[1] == 'FWD':
diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py
index b3b909046d2..d40f06efea3 100755
--- a/src/conf_mode/interfaces_wireless.py
+++ b/src/conf_mode/interfaces_wireless.py
@@ -170,7 +170,7 @@ def verify(wifi):
# 802.11ax (WiFi-6e - HE) can use up to 160MHz bandwidth channels
six_ghz_op_modes_he = ['131', '132', '133', '134', '135']
# 802.11be (WiFi-7 - EHT) can use up to 320MHz bandwidth channels
- six_ghz_op_modes_eht = six_ghz_op_modes_he.append('137')
+ six_ghz_op_modes_he.append('137')
if 'security' in wifi and 'wpa' in wifi['security'] and 'mode' in wifi['security']['wpa']:
if wifi['security']['wpa']['mode'] == 'wpa3':
if 'he' in wifi['capabilities']:
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index bc7925d2864..10d3d78c472 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -419,11 +419,12 @@ def verify(config_dict):
raise ConfigError('route-reflector-client only supported for iBGP peers')
else:
# Check into the peer group for the remote as, if we are in a peer group, check in peer itself
+ peer_group_as = None
if 'peer_group' in peer_config:
peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp)
elif neighbor == 'peer_group':
peer_group_as = peer_config.get('remote_as')
-
+
if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):
raise ConfigError('route-reflector-client only supported for iBGP peers')
diff --git a/src/conf_mode/system_task-scheduler.py b/src/conf_mode/system_task-scheduler.py
index c0253006a41..e03163c9e9c 100755
--- a/src/conf_mode/system_task-scheduler.py
+++ b/src/conf_mode/system_task-scheduler.py
@@ -86,7 +86,7 @@ def verify(tasks):
if task["interval"]:
if task["spec"]:
raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"]))
-
+
if not re.match(r"^\d+[mdh]?$", task["interval"]):
raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"])))
else:
@@ -121,6 +121,7 @@ def generate(tasks):
crontab_lines = []
for task in tasks:
command = make_command(task["executable"], task["args"])
+ line = None
if task["spec"]:
line = format_task(command=command, rawspec=task["spec"])
else:
@@ -131,7 +132,8 @@ def generate(tasks):
line = format_task(command=command, minute="0", hour="*/{0}".format(value))
elif suffix == "d":
line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value))
- crontab_lines.append(line)
+ if line:
+ crontab_lines.append(line)
with open(crontab_file, 'w') as f:
f.write(crontab_header)
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index dcb9f9c6909..e60b58e9ec5 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -400,6 +400,9 @@ def verify(ipsec):
if 'prefix' in pool_config and 'range' in pool_config:
raise ConfigError(f'Only one of prefix or range can be specified for pool "{pool}"!')
+ range_is_ipv4 = False
+ range_is_ipv6 = False
+
if 'prefix' in pool_config:
range_is_ipv4 = is_ipv4(pool_config['prefix'])
range_is_ipv6 = is_ipv6(pool_config['prefix'])
diff --git a/src/migration-scripts/ipoe-server/1-to-2 b/src/migration-scripts/ipoe-server/1-to-2
index cf1cb5ca623..ff24144796d 100644
--- a/src/migration-scripts/ipoe-server/1-to-2
+++ b/src/migration-scripts/ipoe-server/1-to-2
@@ -68,6 +68,7 @@ def migrate(config: ConfigTree) -> None:
namedpools_base = pool_base + ['name']
for pool_name in config.list_nodes(namedpools_base):
+ mask = None
pool_path = namedpools_base + [pool_name]
if config.exists(pool_path + ['subnet']):
subnet = config.return_value(pool_path + ['subnet'])
diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py
index 0da5b3b0b3e..9aeddc06633 100755
--- a/src/op_mode/conntrack_sync.py
+++ b/src/op_mode/conntrack_sync.py
@@ -169,6 +169,8 @@ def show_status(raw: bool):
ct_sync_intf = config.list_nodes(['service', 'conntrack-sync', 'interface'])
ct_sync_intf = ', '.join(ct_sync_intf)
failover_state = "no transition yet!"
+ failover_mechanism = None
+ vrrp_sync_grp = None
expect_sync_protocols = []
if config.exists(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp']):
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index f5b01a323d8..6061884f155 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -100,6 +100,7 @@ def _get_raw_server_leases(
def _get_formatted_server_leases(raw_data, family='inet'):
data_entries = []
+ headers = []
if family == 'inet':
for lease in raw_data:
ipaddr = lease.get('ip')
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 6e9f8a01b76..f226b3652a9 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -192,7 +192,7 @@ def output_firewall_vertical(rules, headers, adjust=True):
print(tabulate.tabulate(transformed_rule, tablefmt="presto"))
print()
-def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=None):
+def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=None, detail=False):
print(f'\n---------------------------------\n{family} Firewall "{hook} {priority}"\n')
details = get_nftables_details(family, hook, priority)
@@ -226,10 +226,10 @@ def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=N
rows.append(row)
if rows:
- if args.rule:
+ if single_rule_id:
rows.pop()
- if args.detail:
+ if detail:
header = ['Rule', 'Description', 'Action', 'Protocol', 'Packets', 'Bytes', 'Conditions']
output_firewall_vertical(rows, header)
else:
@@ -238,7 +238,7 @@ def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=N
rows[rows.index(i)].pop(1)
print(tabulate.tabulate(rows, header) + '\n')
-def output_firewall_state_policy(family):
+def output_firewall_state_policy(family, detail=None):
if family == 'bridge':
return {}
print(f'\n---------------------------------\n{family} State Policy\n')
@@ -254,10 +254,7 @@ def output_firewall_state_policy(family):
rows.append(row)
if rows:
- if args.rule:
- rows.pop()
-
- if args.detail:
+ if detail:
header = ['State', 'Conditions', 'Packets', 'Bytes']
output_firewall_vertical(rows, header)
else:
@@ -266,7 +263,7 @@ def output_firewall_state_policy(family):
rows[rows.index(i)].pop(1)
print(tabulate.tabulate(rows, header) + '\n')
-def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule_id=None):
+def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule_id=None, detail=None):
print(f'\n---------------------------------\n{family} Firewall "{hook} {prior}"\n')
details = get_nftables_details(family, hook, prior)
@@ -384,7 +381,7 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule
rows.append(row)
if rows:
- if args.detail:
+ if detail:
header = ['Rule', 'Description', 'Packets', 'Bytes', 'Action', 'Source', 'Destination', 'Inbound-Interface', 'Outbound-interface']
output_firewall_vertical(rows, header)
else:
@@ -393,7 +390,7 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule
rows[rows.index(i)].pop(1)
print(tabulate.tabulate(rows, header) + '\n')
-def show_firewall():
+def show_firewall(detail=None):
print('Rulesets Information')
conf = Config()
@@ -405,14 +402,14 @@ def show_firewall():
for family in ['ipv4', 'ipv6', 'bridge']:
if 'global_options' in firewall:
if 'state_policy' in firewall['global_options']:
- output_firewall_state_policy(family)
+ output_firewall_state_policy(family, detail=detail)
if family in firewall:
for hook, hook_conf in firewall[family].items():
for prior, prior_conf in firewall[family][hook].items():
- output_firewall_name(family, hook, prior, prior_conf)
+ output_firewall_name(family, hook, prior, prior_conf, detail=detail)
-def show_firewall_family(family):
+def show_firewall_family(family, detail=None):
print(f'Rulesets {family} Information')
conf = Config()
@@ -423,30 +420,30 @@ def show_firewall_family(family):
if 'global_options' in firewall:
if 'state_policy' in firewall['global_options']:
- output_firewall_state_policy(family)
+ output_firewall_state_policy(family, detail=detail)
if family in firewall:
for hook, hook_conf in firewall[family].items():
for prior, prior_conf in firewall[family][hook].items():
- output_firewall_name(family, hook, prior, prior_conf)
+ output_firewall_name(family, hook, prior, prior_conf, detail=detail)
-def show_firewall_name(family, hook, priority):
+def show_firewall_name(family, hook, priority, detail=None):
print('Ruleset Information')
conf = Config()
firewall = get_config_node(conf, 'firewall', family, hook, priority)
if firewall:
- output_firewall_name(family, hook, priority, firewall)
+ output_firewall_name(family, hook, priority, firewall, detail=detail)
-def show_firewall_rule(family, hook, priority, rule_id):
+def show_firewall_rule(family, hook, priority, rule_id, detail=None):
print('Rule Information')
conf = Config()
firewall = get_config_node(conf, 'firewall', family, hook, priority)
if firewall:
- output_firewall_name(family, hook, priority, firewall, rule_id)
+ output_firewall_name(family, hook, priority, firewall, rule_id, detail=detail)
-def show_firewall_group(name=None):
+def show_firewall_group(name=None, detail=None):
conf = Config()
firewall = get_config_node(conf, node='firewall')
@@ -594,7 +591,7 @@ def find_references(group_type, group_name):
for group_type, group_type_conf in firewall['group'].items():
# interate over dynamic-groups
if group_type == 'dynamic_group':
- if not args.detail:
+ if not detail:
header_tail = ['Timeout', 'Expires']
for dynamic_type in ['address_group', 'ipv6_address_group']:
@@ -611,7 +608,7 @@ def find_references(group_type, group_name):
members = get_nftables_group_members(family, 'vyos_filter', f'{prefix}{dynamic_name}')
if not members:
- if args.detail:
+ if detail:
row.append('N/D')
else:
row += ["N/D"] * 3
@@ -629,7 +626,7 @@ def find_references(group_type, group_name):
timeout = str(member.get('timeout', 'N/D'))
expires = str(member.get('expires', 'N/D'))
- if args.detail:
+ if detail:
row.append(f'{val} (timeout: {timeout}, expires: {expires})')
continue
@@ -639,7 +636,7 @@ def find_references(group_type, group_name):
row += [val, timeout, expires]
rows.append(row)
- if args.detail:
+ if detail:
header_tail += [""] * (len(members) - 1)
rows.append(row)
@@ -657,7 +654,7 @@ def find_references(group_type, group_name):
if 'url' in remote_conf:
# display only the url if no members are found for both views
if not members and not members6:
- if args.detail:
+ if detail:
header_tail = ['IPv6 Members', 'Remote URL']
row.append('N/D')
row.append('N/D')
@@ -667,7 +664,7 @@ def find_references(group_type, group_name):
rows.append(row)
else:
# display all table elements in detail view
- if args.detail:
+ if detail:
header_tail = ['IPv6 Members', 'Remote URL']
if members:
row.append(' '.join(members))
@@ -707,7 +704,7 @@ def find_references(group_type, group_name):
if rows:
print('Firewall Groups\n')
- if args.detail:
+ if detail:
header = ['Name', 'Description', 'Type', 'References', 'Members'] + header_tail
output_firewall_vertical(rows, header, adjust=False)
else:
@@ -716,7 +713,7 @@ def find_references(group_type, group_name):
rows[rows.index(i)].pop(1)
print(tabulate.tabulate(rows, header))
-def show_summary():
+def show_summary(detail=None):
print('Ruleset Summary')
conf = Config()
@@ -760,9 +757,9 @@ def show_summary():
print('\nBridge Ruleset:\n')
print(tabulate.tabulate(br_out, header) + '\n')
- show_firewall_group()
+ show_firewall_group(detail=detail)
-def show_statistics():
+def show_statistics(detail=None):
print('Rulesets Statistics')
conf = Config()
@@ -779,7 +776,7 @@ def show_statistics():
if family in firewall:
for hook, hook_conf in firewall[family].items():
for prior, prior_conf in firewall[family][hook].items():
- output_firewall_name_statistics(family, hook,prior, prior_conf)
+ output_firewall_name_statistics(family, hook,prior, prior_conf, detail=detail)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
@@ -796,16 +793,16 @@ def show_statistics():
if args.action == 'show':
if not args.rule:
- show_firewall_name(args.family, args.hook, args.priority)
+ show_firewall_name(args.family, args.hook, args.priority, args.detail)
else:
- show_firewall_rule(args.family, args.hook, args.priority, args.rule)
+ show_firewall_rule(args.family, args.hook, args.priority, args.rule, args.detail)
elif args.action == 'show_all':
- show_firewall()
+ show_firewall(args.detail)
elif args.action == 'show_family':
- show_firewall_family(args.family)
+ show_firewall_family(args.family, args.detail)
elif args.action == 'show_group':
- show_firewall_group(args.name)
+ show_firewall_group(args.name, args.detail)
elif args.action == 'show_statistics':
- show_statistics()
+ show_statistics(args.detail)
elif args.action == 'show_summary':
- show_summary()
+ show_summary(args.detail)
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
index 0db9ef545e3..6c1c0781289 100755
--- a/src/op_mode/ikev2_profile_generator.py
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -225,6 +225,7 @@
def transform_pfs(pfs, ike_dh_group):
pfs_enabled = (pfs != 'disable')
+ pfs_dh_group = None
if pfs == 'enable':
pfs_dh_group = ike_dh_group
elif pfs.startswith('dh-group'):
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index a65bcfd5975..61018b0bc49 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -36,6 +36,7 @@ def _get_xml_translation(direction, family, address=None):
"""
Get conntrack XML output --src-nat|--dst-nat
"""
+ opt = ''
if direction == 'source':
opt = '--src-nat'
if direction == 'destination':
@@ -62,6 +63,7 @@ def _get_json_data(direction, family):
"""
Get NAT format JSON
"""
+ chain = ''
if direction == 'source':
chain = 'POSTROUTING'
if direction == 'destination':
@@ -137,6 +139,7 @@ def _get_ports_for_output(rules):
data_entries = []
for rule in data:
+ interface = 'any'
if 'comment' in rule['rule']:
comment = rule.get('rule').get('comment')
rule_number = comment.split('-')[-1]
@@ -239,6 +242,9 @@ def _get_ports_for_output(rules):
def _get_formatted_output_statistics(data, direction):
data_entries = []
for rule in data:
+ packets = ''
+ _bytes = ''
+ interface = 'any'
if 'comment' in rule['rule']:
comment = rule.get('rule').get('comment')
rule_number = comment.split('-')[-1]
diff --git a/src/op_mode/otp.py b/src/op_mode/otp.py
index aceb7566067..f9fb9cc5a03 100755
--- a/src/op_mode/otp.py
+++ b/src/op_mode/otp.py
@@ -85,6 +85,7 @@ def _get_login_otp(username: str, info:str):
result['otp_length'] = '6'
result['interval'] = '30'
result['token_type'] = 'hotp-time'
+ token_type_acrn = ''
if result['token_type'] == 'hotp-time':
token_type_acrn = 'totp'
result['otp_url'] = ''.join(["otpauth://",token_type_acrn,"/",username,"@",\
diff --git a/src/op_mode/show_openconnect_otp.py b/src/op_mode/show_openconnect_otp.py
index 36aa7bf1101..8366df3738e 100755
--- a/src/op_mode/show_openconnect_otp.py
+++ b/src/op_mode/show_openconnect_otp.py
@@ -63,6 +63,7 @@ def display_otp_ocserv(username, params, info):
otp_length = params['otp']['otp_length']
interval = params['otp']['interval']
token_type = params['otp']['token_type']
+ token_type_acrn = ''
if token_type == 'hotp-time':
token_type_acrn = 'totp'
key_base32 = b32encode(bytes.fromhex(key_hex)).decode()
diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py
index 05e7d0e752c..4106051ab97 100755
--- a/src/op_mode/show_wwan.py
+++ b/src/op_mode/show_wwan.py
@@ -37,7 +37,7 @@
def qmi_cmd(device, command, silent=False):
try:
tmp = cmd(f'qmicli --device={device} --device-open-proxy {command}')
- tmp = tmp.replace(f'[{cdc}] ', '')
+ tmp = tmp.replace(f'[{device}] ', '')
if not silent:
# skip first line as this only holds the info headline
for line in tmp.splitlines()[1:]:
From c6bef37f7fe75310e63292e32a6d2735b1f282b4 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 3 Aug 2025 22:38:02 +0200
Subject: [PATCH 17/17] T7557: Update locked user check without spwd module
---
src/op_mode/show_users.py | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/src/op_mode/show_users.py b/src/op_mode/show_users.py
index bccfaf99172..cc650780e08 100755
--- a/src/op_mode/show_users.py
+++ b/src/op_mode/show_users.py
@@ -47,13 +47,14 @@ def is_locked(user_name: str) -> bool:
"""Check if a given user has password in shadow db"""
try:
- import warnings
- with warnings.catch_warnings():
- warnings.filterwarnings("ignore",category=DeprecationWarning)
- import spwd
- encrypted_password = spwd.getspnam(user_name)[1]
- return encrypted_password == '*' or encrypted_password.startswith('!')
- except (KeyError, PermissionError):
+ with open('/etc/shadow', 'r') as f:
+ for line in f.readlines():
+ args = line.strip().split(':')
+ if len(args) < 2:
+ continue
+ if args[0] == user_name:
+ return len(args[1]) > 0 and args[1].startswith('!')
+ except PermissionError:
print('Cannot access shadow database, ensure this script is run with sufficient permissions')
sys.exit(1)