summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/container.py3
-rwxr-xr-xsrc/conf_mode/interfaces_bridge.py8
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py17
-rwxr-xr-xsrc/conf_mode/vrf.py7
-rwxr-xr-xsrc/helpers/reset_section.py124
-rw-r--r--src/migration-scripts/container/2-to-331
-rw-r--r--src/migration-scripts/firewall/18-to-1935
-rw-r--r--src/migration-scripts/quagga/8-to-918
-rwxr-xr-xsrc/op_mode/container.py42
-rwxr-xr-xsrc/services/vyos-configd14
-rw-r--r--src/shim/vyshim.c41
-rw-r--r--src/tests/test_template.py4
12 files changed, 325 insertions, 19 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 83e6dee11..a381ace5c 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -310,6 +310,7 @@ def generate_run_arguments(name, container_config):
memory = container_config['memory']
shared_memory = container_config['shared_memory']
restart = container_config['restart']
+ log_driver = container_config['log_driver']
# Add sysctl options
sysctl_opt = ''
@@ -408,7 +409,7 @@ def generate_run_arguments(name, container_config):
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} ' \
+ 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 = ''
diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py
index c14e6a599..fce07ae0a 100755
--- a/src/conf_mode/interfaces_bridge.py
+++ b/src/conf_mode/interfaces_bridge.py
@@ -111,6 +111,11 @@ def get_config(config=None):
elif interface.startswith('wlan') and interface_exists(interface):
set_dependents('wlan', conf, interface)
+ if interface.startswith('vtun'):
+ _, tmp_config = get_interface_dict(conf, ['interfaces', 'openvpn'], interface)
+ tmp = tmp_config.get('device_type') == 'tap'
+ bridge['member']['interface'][interface].update({'valid_ovpn' : tmp})
+
# delete empty dictionary keys - no need to run code paths if nothing is there to do
if 'member' in bridge:
if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0:
@@ -178,6 +183,9 @@ def verify(bridge):
if option in interface_config:
raise ConfigError('Can not use VLAN options on non VLAN aware bridge')
+ if interface.startswith('vtun') and not interface_config['valid_ovpn']:
+ raise ConfigError(error_msg + 'OpenVPN device-type must be set to "tap"')
+
if 'enable_vlan' in bridge:
if dict_search('vif.1', bridge):
raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface')
diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
index ef0250e3d..054aa1c0e 100755
--- a/src/conf_mode/protocols_rpki.py
+++ b/src/conf_mode/protocols_rpki.py
@@ -18,6 +18,7 @@ import os
from glob import glob
from sys import exit
+from sys import argv
from vyos.config import Config
from vyos.configverify import has_frr_protocol_in_dict
@@ -39,13 +40,18 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- return get_frrender_dict(conf)
+ return get_frrender_dict(conf, argv)
def verify(config_dict):
if not has_frr_protocol_in_dict(config_dict, 'rpki'):
return None
- rpki = config_dict['rpki']
+ vrf = None
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ rpki = vrf and config_dict['vrf']['name'][vrf]['protocols']['rpki'] or config_dict['rpki']
if 'cache' in rpki:
preferences = []
@@ -79,7 +85,12 @@ def generate(config_dict):
if not has_frr_protocol_in_dict(config_dict, 'rpki'):
return None
- rpki = config_dict['rpki']
+ vrf = None
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ rpki = vrf and config_dict['vrf']['name'][vrf]['protocols']['rpki'] or config_dict['rpki']
if 'cache' in rpki:
for cache, cache_config in rpki['cache'].items():
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 8baf55857..6e9d4147a 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -18,6 +18,8 @@ from sys import exit
from jmespath import search
from json import loads
+import vyos.defaults
+
from vyos.config import Config
from vyos.configdict import node_changed
from vyos.configverify import verify_route_map
@@ -163,6 +165,11 @@ def verify(vrf):
if 'table' not in vrf_config:
raise ConfigError(f'VRF "{name}" table id is mandatory!')
+ if int(vrf_config['table']) == vyos.defaults.rt_global_vrf:
+ raise ConfigError(
+ f'VRF "{name}" table id {vrf_config["table"]} cannot be used!'
+ )
+
# routing table id can't be changed - OS restriction
if interface_exists(name):
tmp = get_vrf_tableid(name)
diff --git a/src/helpers/reset_section.py b/src/helpers/reset_section.py
new file mode 100755
index 000000000..32857f650
--- /dev/null
+++ b/src/helpers/reset_section.py
@@ -0,0 +1,124 @@
+#!/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 sys
+import os
+import grp
+
+from vyos.configsession import ConfigSession
+from vyos.config import Config
+from vyos.configdiff import get_config_diff
+from vyos.xml_ref import is_leaf
+
+
+CFG_GROUP = 'vyattacfg'
+DEBUG = False
+
+
+def type_str_to_list(value):
+ if isinstance(value, str):
+ return value.split()
+ raise argparse.ArgumentTypeError('path must be a whitespace separated string')
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument('path', type=type_str_to_list, help='section to reload/rollback')
+parser.add_argument('--pid', help='pid of config session')
+
+group = parser.add_mutually_exclusive_group()
+group.add_argument('--reload', action='store_true', help='retry proposed commit')
+group.add_argument(
+ '--rollback', action='store_true', default=True, help='rollback to stable commit'
+)
+
+args = parser.parse_args()
+
+path = args.path
+reload = args.reload
+rollback = args.rollback
+pid = args.pid
+
+try:
+ if is_leaf(path):
+ sys.exit('path is leaf node: neither allowed nor useful')
+except ValueError:
+ if DEBUG:
+ sys.exit('nonexistent path: neither allowed nor useful')
+ else:
+ sys.exit()
+
+test = Config()
+in_session = test.in_session()
+
+if in_session:
+ if reload:
+ sys.exit('reset_section reload not available inside of a config session')
+
+ diff = get_config_diff(test)
+ if not diff.is_node_changed(path):
+ # No discrepancies at path after commit, hence no error to revert.
+ sys.exit()
+
+ del diff
+else:
+ if not reload:
+ sys.exit('reset_section rollback not available outside of a config session')
+
+del test
+
+
+session_id = int(pid) if pid else os.getppid()
+
+if in_session:
+ # check hint left by vyshim when ConfigError is from apply stage
+ hint_name = f'/tmp/apply_{session_id}'
+ if not os.path.exists(hint_name):
+ # no apply error; exit
+ sys.exit()
+ else:
+ # cleanup hint and continue with reset
+ os.unlink(hint_name)
+
+cfg_group = grp.getgrnam(CFG_GROUP)
+os.setgid(cfg_group.gr_gid)
+os.umask(0o002)
+
+shared = not bool(reload)
+
+session = ConfigSession(session_id, shared=shared)
+
+session_env = session.get_session_env()
+config = Config(session_env)
+
+d = config.get_config_dict(path, effective=True, get_first_key=True)
+
+if in_session:
+ session.discard()
+
+session.delete(path)
+session.commit()
+
+if not d:
+ # nothing more to do in either case of reload/rollback
+ sys.exit()
+
+session.set_section(path, d)
+out = session.commit()
+print(out)
diff --git a/src/migration-scripts/container/2-to-3 b/src/migration-scripts/container/2-to-3
new file mode 100644
index 000000000..54c6ec4c8
--- /dev/null
+++ b/src/migration-scripts/container/2-to-3
@@ -0,0 +1,31 @@
+# Copyright 2024 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/>.
+
+# T7473: container: allow log-driver to be set per container
+
+from vyos.configtree import ConfigTree
+
+def migrate(config: ConfigTree) -> None:
+ log_base = ['container', 'log-driver']
+ container_base = ['container', 'name']
+
+ if not config.exists(log_base):
+ return
+ else:
+ log_driver = config.return_value(log_base)
+ for container in config.list_nodes(container_base):
+ # Set the log-driver for each container
+ config.set(container_base + [container, 'log-driver'], value=log_driver)
+ config.delete(log_base)
diff --git a/src/migration-scripts/firewall/18-to-19 b/src/migration-scripts/firewall/18-to-19
new file mode 100644
index 000000000..3564e0e01
--- /dev/null
+++ b/src/migration-scripts/firewall/18-to-19
@@ -0,0 +1,35 @@
+# Copyright (C) 2024-2025 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 <http://www.gnu.org/licenses/>.
+
+# From
+# set firewall global-options apply-to-bridged-traffic invalid-connections
+# To
+# set firewall global-options apply-to-bridged-traffic accept-invalid ethernet-type <ethertype>
+
+from vyos.configtree import ConfigTree
+
+base = ['firewall', 'global-options', 'apply-to-bridged-traffic']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base + ['invalid-connections']):
+ # Nothing to do
+ return
+
+ ether_types = ['dhcp', 'arp', 'pppoe-discovery', 'pppoe', '802.1q', '802.1ad', 'wol']
+
+ for ether_type in ether_types:
+ config.set(base + ['accept-invalid', 'ethernet-type'], value=ether_type, replace=False)
+
+ config.delete(base + ['invalid-connections'])
diff --git a/src/migration-scripts/quagga/8-to-9 b/src/migration-scripts/quagga/8-to-9
index eece6c15d..c28e07e5c 100644
--- a/src/migration-scripts/quagga/8-to-9
+++ b/src/migration-scripts/quagga/8-to-9
@@ -16,6 +16,7 @@
# - T2450: drop interface-route and interface-route6 from "protocols static"
from vyos.configtree import ConfigTree
+from vyos.template import is_ip
def migrate_interface_route(config, base, path, route_route6):
""" Generic migration function which can be called on every instance of
@@ -31,11 +32,18 @@ def migrate_interface_route(config, base, path, route_route6):
tmp = base + path + [route, 'next-hop-interface']
for interface in config.list_nodes(tmp):
- new_base = base + [route_route6, route, 'interface']
- config.set(new_base)
- config.set_tag(base + [route_route6])
- config.set_tag(new_base)
- config.copy(tmp + [interface], new_base + [interface])
+ if is_ip(interface): # not prohibited in 1.3.x, hence allowed
+ new_base = base + [route_route6, route, 'next-hop']
+ config.set(new_base)
+ config.set_tag(base + [route_route6])
+ config.set_tag(new_base)
+ config.copy(tmp + [interface], new_base + [interface])
+ else:
+ new_base = base + [route_route6, route, 'interface']
+ config.set(new_base)
+ config.set_tag(base + [route_route6])
+ config.set_tag(new_base)
+ config.copy(tmp + [interface], new_base + [interface])
config.delete(base + path)
diff --git a/src/op_mode/container.py b/src/op_mode/container.py
index 05f65df1f..f93df0fc4 100755
--- a/src/op_mode/container.py
+++ b/src/op_mode/container.py
@@ -16,6 +16,7 @@
import json
import sys
+import subprocess
from vyos.utils.process import cmd
from vyos.utils.process import rc_cmd
@@ -109,6 +110,47 @@ def restart(name: str):
print(f'Container "{name}" restarted!')
return output
+def show_log(name: str, follow: bool = False, raw: bool = False):
+ """
+ Show or monitor logs for a specific container.
+ Use --follow to continuously stream logs.
+ """
+ from vyos.configquery import ConfigTreeQuery
+ conf = ConfigTreeQuery()
+ container = conf.get_config_dict(['container', 'name', name], get_first_key=True, with_recursive_defaults=True)
+ log_type = container.get('log-driver')
+ if log_type == 'k8s-file':
+ if follow:
+ log_command_list = ['sudo', 'podman', 'logs', '--follow', '--names', name]
+ else:
+ log_command_list = ['sudo', 'podman', 'logs', '--names', name]
+ elif log_type == 'journald':
+ if follow:
+ log_command_list = ['journalctl', '--follow', '--unit', f'vyos-container-{name}.service']
+ else:
+ log_command_list = ['journalctl', '-e', '--no-pager', '--unit', f'vyos-container-{name}.service']
+ elif log_type == 'none':
+ print(f'Container "{name}" has disabled logs.')
+ return None
+ else:
+ raise vyos.opmode.InternalError(f'Unknown log type "{log_type}" for container "{name}".')
+
+ process = None
+ try:
+ process = subprocess.Popen(log_command_list,
+ stdout=sys.stdout,
+ stderr=sys.stderr)
+ process.wait()
+ except KeyboardInterrupt:
+ if process:
+ process.terminate()
+ process.wait()
+ return None
+ except Exception as e:
+ raise vyos.opmode.InternalError(f"Error starting logging command: {e} ")
+ return None
+
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index 28acccd2c..c45d492f9 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -68,6 +68,7 @@ class Response(Enum):
ERROR_COMMIT = 2
ERROR_DAEMON = 4
PASS = 8
+ ERROR_COMMIT_APPLY = 16
vyos_conf_scripts_dir = directories['conf_mode']
@@ -142,8 +143,6 @@ def run_script(script_name, config, args) -> tuple[Response, str]:
try:
c = script.get_config(config)
script.verify(c)
- script.generate(c)
- script.apply(c)
except ConfigError as e:
logger.error(e)
return Response.ERROR_COMMIT, str(e)
@@ -152,6 +151,17 @@ def run_script(script_name, config, args) -> tuple[Response, str]:
logger.error(tb)
return Response.ERROR_COMMIT, tb
+ try:
+ script.generate(c)
+ script.apply(c)
+ except ConfigError as e:
+ logger.error(e)
+ return Response.ERROR_COMMIT_APPLY, str(e)
+ except Exception:
+ tb = traceback.format_exc()
+ logger.error(tb)
+ return Response.ERROR_COMMIT_APPLY, tb
+
return Response.SUCCESS, ''
diff --git a/src/shim/vyshim.c b/src/shim/vyshim.c
index 1eb653cbf..35f995419 100644
--- a/src/shim/vyshim.c
+++ b/src/shim/vyshim.c
@@ -18,8 +18,10 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
+#include <fcntl.h>
#include <unistd.h>
#include <string.h>
+#include <sys/stat.h>
#include <sys/time.h>
#include <time.h>
#include <stdint.h>
@@ -55,15 +57,17 @@ enum {
SUCCESS = 1 << 0,
ERROR_COMMIT = 1 << 1,
ERROR_DAEMON = 1 << 2,
- PASS = 1 << 3
+ PASS = 1 << 3,
+ ERROR_COMMIT_APPLY = 1 << 4
};
volatile int init_alarm = 0;
volatile int timeout = 0;
-int initialization(void *);
+int initialization(void *, char *);
int pass_through(char **, int);
void timer_handler(int);
+void leave_hint(char *);
double get_posix_clock_time(void);
@@ -94,8 +98,17 @@ int main(int argc, char* argv[])
char *test = strstr(string_node_data, "VYOS_TAGNODE_VALUE");
ex_index = test ? 2 : 1;
+ char *env_tmp = getenv("VYATTA_CONFIG_TMP");
+ if (env_tmp == NULL) {
+ fprintf(stderr, "Error: Environment variable VYATTA_CONFIG_TMP is not set.\n");
+ exit(EXIT_FAILURE);
+ }
+ char *pid_str = strdup(env_tmp);
+ strsep(&pid_str, "_");
+ debug_print("config session pid: %s\n", pid_str);
+
if (access(COMMIT_MARKER, F_OK) != -1) {
- init_timeout = initialization(requester);
+ init_timeout = initialization(requester, pid_str);
if (!init_timeout) remove(COMMIT_MARKER);
}
@@ -151,13 +164,19 @@ int main(int argc, char* argv[])
ret = -1;
}
+ if (err & ERROR_COMMIT_APPLY) {
+ debug_print("Received ERROR_COMMIT_APPLY\n");
+ leave_hint(pid_str);
+ ret = -1;
+ }
+
zmq_close(requester);
zmq_ctx_destroy(context);
return ret;
}
-int initialization(void* Requester)
+int initialization(void* Requester, char* pid_val)
{
char *active_str = NULL;
size_t active_len = 0;
@@ -185,10 +204,6 @@ int initialization(void* Requester)
double prev_time_value, time_value;
double time_diff;
- char *pid_val = getenv("VYATTA_CONFIG_TMP");
- strsep(&pid_val, "_");
- debug_print("config session pid: %s\n", pid_val);
-
char *sudo_user = getenv("SUDO_USER");
if (!sudo_user) {
char nobody[] = "nobody";
@@ -338,6 +353,16 @@ void timer_handler(int signum)
return;
}
+void leave_hint(char *pid_val)
+{
+ char tmp_str[16];
+ mode_t omask = umask(0);
+ snprintf(tmp_str, sizeof(tmp_str), "/tmp/apply_%s", pid_val);
+ open(tmp_str, O_CREAT|O_RDWR|O_TRUNC, 0666);
+ chown(tmp_str, 1002, 102);
+ umask(omask);
+}
+
#ifdef _POSIX_MONOTONIC_CLOCK
double get_posix_clock_time(void)
{
diff --git a/src/tests/test_template.py b/src/tests/test_template.py
index 4660c0038..09315d398 100644
--- a/src/tests/test_template.py
+++ b/src/tests/test_template.py
@@ -199,8 +199,12 @@ class TestVyOSTemplate(TestCase):
vyos.template.get_default_config_file('UNKNOWN')
with self.assertRaises(RuntimeError):
vyos.template.get_default_port('UNKNOWN')
+ with self.assertRaises(RuntimeError):
+ vyos.template.nft_accept_invalid('UNKNOWN')
self.assertEqual(vyos.template.get_default_config_file('sshd_user_ca'),
config_files['sshd_user_ca'])
self.assertEqual(vyos.template.get_default_port('certbot_haproxy'),
internal_ports['certbot_haproxy'])
+ self.assertEqual(vyos.template.nft_accept_invalid('arp'),
+ 'ct state invalid ether type arp counter accept')