summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/lint-with-darker-ruff.yml14
-rw-r--r--data/templates/https/nginx.default.j22
-rw-r--r--data/templates/openvpn/server.conf.j212
-rw-r--r--data/templates/router-advert/radvd.conf.j22
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in28
-rw-r--r--interface-definitions/service_router-advert.xml.in6
-rw-r--r--op-mode-definitions/ntp.xml.in8
-rw-r--r--op-mode-definitions/show-interfaces-macsec.xml.in8
-rw-r--r--python/vyos/configdep.py26
-rw-r--r--python/vyos/configdiff.py37
-rw-r--r--python/vyos/configsession.py11
-rw-r--r--python/vyos/pki.py2
-rw-r--r--python/vyos/utils/dict.py1
-rw-r--r--python/vyos/xml_ref/__init__.py6
-rw-r--r--python/vyos/xml_ref/definition.py22
-rwxr-xr-xpython/vyos/xml_ref/generate_op_cache.py4
-rwxr-xr-xsmoketest/scripts/cli/test_config_dependency.py85
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py186
-rwxr-xr-xsmoketest/scripts/cli/test_service_router-advert.py29
-rwxr-xr-xsmoketest/scripts/cli/test_system_option.py15
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py37
-rwxr-xr-xsrc/conf_mode/system_option.py2
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper18
-rw-r--r--src/op_mode/ntp.py45
-rwxr-xr-xsrc/op_mode/pki.py41
-rwxr-xr-xsrc/services/vyos-configd19
-rwxr-xr-xsrc/services/vyos-http-api-server62
27 files changed, 669 insertions, 59 deletions
diff --git a/.github/workflows/lint-with-darker-ruff.yml b/.github/workflows/lint-with-darker-ruff.yml
new file mode 100644
index 000000000..01f7cd448
--- /dev/null
+++ b/.github/workflows/lint-with-darker-ruff.yml
@@ -0,0 +1,14 @@
+name: Lint py code with darker and ruff
+on:
+ pull_request_target:
+ branches:
+ - current
+
+permissions:
+ pull-requests: write
+ contents: read
+
+jobs:
+ darker-ruff-lint:
+ uses: vyos/.github/.github/workflows/lint-with-darker-ruff.yml@current
+ secrets: inherit
diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2
index 4619361e5..1dde66ebf 100644
--- a/data/templates/https/nginx.default.j2
+++ b/data/templates/https/nginx.default.j2
@@ -48,7 +48,7 @@ server {
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
# proxy settings for HTTP API, if enabled; 503, if not
- location ~ ^/(retrieve|configure|config-file|image|container-image|generate|show|reboot|reset|poweroff|docs|openapi.json|redoc|graphql) {
+ location ~ ^/(retrieve|configure|config-file|image|import-pki|container-image|generate|show|reboot|reset|poweroff|docs|openapi.json|redoc|graphql) {
{% if api is vyos_defined %}
proxy_pass http://unix:/run/api.sock;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2
index 408103558..be811f45e 100644
--- a/data/templates/openvpn/server.conf.j2
+++ b/data/templates/openvpn/server.conf.j2
@@ -11,11 +11,11 @@ dev-type {{ device_type }}
dev {{ ifname }}
persist-key
{% if protocol is vyos_defined('tcp-active') %}
-proto tcp-client
+proto tcp{{ protocol_modifier }}-client
{% elif protocol is vyos_defined('tcp-passive') %}
-proto tcp-server
+proto tcp{{ protocol_modifier }}-server
{% else %}
-proto udp
+proto udp{{ protocol_modifier }}
{% endif %}
{% if local_host is vyos_defined %}
local {{ local_host }}
@@ -63,6 +63,9 @@ nobind
#
# OpenVPN Server mode
#
+{% if ip_version is vyos_defined('ipv6') %}
+bind ipv6only
+{% endif %}
mode server
tls-server
{% if server is vyos_defined %}
@@ -131,6 +134,9 @@ plugin "{{ plugin_dir }}/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{{ if
#
# OpenVPN site-2-site mode
#
+{% if ip_version is vyos_defined('ipv6') %}
+bind ipv6only
+{% endif %}
ping {{ keep_alive.interval }}
ping-restart {{ keep_alive.failure_count }}
diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2
index 97180d164..a83bd03ac 100644
--- a/data/templates/router-advert/radvd.conf.j2
+++ b/data/templates/router-advert/radvd.conf.j2
@@ -19,7 +19,7 @@ interface {{ iface }} {
{% if iface_config.reachable_time is vyos_defined %}
AdvReachableTime {{ iface_config.reachable_time }};
{% endif %}
- AdvIntervalOpt {{ 'off' if iface_config.no_send_advert is vyos_defined else 'on' }};
+ AdvIntervalOpt {{ 'off' if iface_config.no_send_interval is vyos_defined else 'on' }};
AdvSendAdvert {{ 'off' if iface_config.no_send_advert is vyos_defined else 'on' }};
{% if iface_config.default_lifetime is vyos_defined %}
AdvDefaultLifetime {{ iface_config.default_lifetime }};
diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in
index 3563caef2..3c844107e 100644
--- a/interface-definitions/interfaces_openvpn.xml.in
+++ b/interface-definitions/interfaces_openvpn.xml.in
@@ -318,6 +318,34 @@
</properties>
<defaultValue>udp</defaultValue>
</leafNode>
+ <leafNode name="ip-version">
+ <properties>
+ <help>Force OpenVPN to use a specific IP protocol version</help>
+ <completionHelp>
+ <list>auto ipv4 ipv6 dual-stack</list>
+ </completionHelp>
+ <valueHelp>
+ <format>auto</format>
+ <description>Select one IP protocol to use based on local or remote host</description>
+ </valueHelp>
+ <valueHelp>
+ <format>_ipv4</format>
+ <description>Accept connections on or initate connections to IPv4 addresses only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>_ipv6</format>
+ <description>Accept connections on or initate connections to IPv6 addresses only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dual-stack</format>
+ <description>Accept connections on both protocols simultaneously (only supported in server mode)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(auto|ipv4|ipv6|dual-stack)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>auto</defaultValue>
+ </leafNode>
<leafNode name="remote-address">
<properties>
<help>IP address of remote end of tunnel</help>
diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in
index 166a4a0cf..3fd33540a 100644
--- a/interface-definitions/service_router-advert.xml.in
+++ b/interface-definitions/service_router-advert.xml.in
@@ -390,6 +390,12 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="no-send-interval">
+ <properties>
+ <help>Do not send Advertisement Interval option in RAs</help>
+ <valueless/>
+ </properties>
+ </leafNode>
</children>
</tagNode>
</children>
diff --git a/op-mode-definitions/ntp.xml.in b/op-mode-definitions/ntp.xml.in
index 17250a45e..565a5edb5 100644
--- a/op-mode-definitions/ntp.xml.in
+++ b/op-mode-definitions/ntp.xml.in
@@ -6,25 +6,25 @@
<properties>
<help>Show peer status of NTP daemon</help>
</properties>
- <command>${vyos_op_scripts_dir}/ntp.py show_sourcestats</command>
+ <command>sudo ${vyos_op_scripts_dir}/ntp.py show_sourcestats</command>
<children>
<node name="activity">
<properties>
<help>Report the number of servers and peers that are online and offline</help>
</properties>
- <command>${vyos_op_scripts_dir}/ntp.py show_activity</command>
+ <command>sudo ${vyos_op_scripts_dir}/ntp.py show_activity</command>
</node>
<node name="sources">
<properties>
<help>Show information about the current time sources being accessed</help>
</properties>
- <command>${vyos_op_scripts_dir}/ntp.py show_sources</command>
+ <command>sudo ${vyos_op_scripts_dir}/ntp.py show_sources</command>
</node>
<node name="system">
<properties>
<help>Show parameters about the system clock performance</help>
</properties>
- <command>${vyos_op_scripts_dir}/ntp.py show_tracking</command>
+ <command>sudo ${vyos_op_scripts_dir}/ntp.py show_tracking</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/show-interfaces-macsec.xml.in b/op-mode-definitions/show-interfaces-macsec.xml.in
index a264ff22e..28264d252 100644
--- a/op-mode-definitions/show-interfaces-macsec.xml.in
+++ b/op-mode-definitions/show-interfaces-macsec.xml.in
@@ -12,6 +12,14 @@
</completionHelp>
</properties>
<command>ip macsec show</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed MACsec interface information</help>
+ </properties>
+ <command>ip -s macsec show</command>
+ </leafNode>
+ </children>
</node>
<tagNode name="macsec">
<properties>
diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py
index e0fe1ddac..cf7c9d543 100644
--- a/python/vyos/configdep.py
+++ b/python/vyos/configdep.py
@@ -95,7 +95,8 @@ def get_dependency_dict(config: 'Config') -> dict:
setattr(config, 'cached_dependency_dict', d)
return d
-def run_config_mode_script(script: str, config: 'Config'):
+def run_config_mode_script(target: str, config: 'Config'):
+ script = target + '.py'
path = os.path.join(directories['conf_mode'], script)
name = canon_name(script)
mod = load_as_module(name, path)
@@ -109,15 +110,34 @@ def run_config_mode_script(script: str, config: 'Config'):
except (VyOSError, ConfigError) as e:
raise ConfigError(str(e)) from e
+def run_conditionally(target: str, tagnode: str, config: 'Config'):
+ tag_ext = f'_{tagnode}' if tagnode else ''
+ script_name = f'{target}{tag_ext}'
+
+ scripts_called = getattr(config, 'scripts_called', [])
+ commit_scripts = getattr(config, 'commit_scripts', [])
+
+ debug_print(f'scripts_called: {scripts_called}')
+ debug_print(f'commit_scripts: {commit_scripts}')
+
+ if script_name in commit_scripts and script_name not in scripts_called:
+ debug_print(f'dependency {script_name} deferred to priority')
+ return
+
+ run_config_mode_script(target, config)
+
def def_closure(target: str, config: 'Config',
tagnode: typing.Optional[str] = None) -> typing.Callable:
- script = target + '.py'
def func_impl():
+ tag_value = ''
if tagnode is not None:
os.environ['VYOS_TAGNODE_VALUE'] = tagnode
- run_config_mode_script(script, config)
+ tag_value = tagnode
+ run_conditionally(target, tag_value, config)
+
tag_ext = f'_{tagnode}' if tagnode is not None else ''
func_impl.__name__ = f'{target}{tag_ext}'
+
return func_impl
def set_dependents(case: str, config: 'Config',
diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py
index f975df45d..b6d4a5558 100644
--- a/python/vyos/configdiff.py
+++ b/python/vyos/configdiff.py
@@ -15,6 +15,7 @@
from enum import IntFlag
from enum import auto
+from itertools import chain
from vyos.config import Config
from vyos.configtree import DiffTree
@@ -22,7 +23,10 @@ from vyos.configdict import dict_merge
from vyos.utils.dict import get_sub_dict
from vyos.utils.dict import mangle_dict_keys
from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_to_key_paths
from vyos.xml_ref import get_defaults
+from vyos.xml_ref import owner
+from vyos.xml_ref import priority
class ConfigDiffError(Exception):
"""
@@ -94,6 +98,39 @@ def get_config_diff(config, key_mangling=None):
return ConfigDiff(config, key_mangling, diff_tree=diff_t,
diff_dict=diff_d)
+def get_commit_scripts(config) -> list:
+ """Return the list of config scripts to be executed by commit
+
+ Return a list of the scripts to be called by commit for the proposed
+ config. The list is ordered by priority for reference, however, the
+ actual order of execution by the commit algorithm is not reflected
+ (delete vs. add queue), nor needed for current use.
+ """
+ if not config or not isinstance(config, Config):
+ raise TypeError("argument must me a Config instance")
+
+ if hasattr(config, 'commit_scripts'):
+ return getattr(config, 'commit_scripts')
+
+ D = get_config_diff(config)
+ d = D._diff_dict
+ s = set()
+ for p in chain(dict_to_key_paths(d['sub']), dict_to_key_paths(d['add'])):
+ p_owner = owner(p, with_tag=True)
+ if not p_owner:
+ continue
+ p_priority = priority(p)
+ if not p_priority:
+ # default priority in legacy commit-algorithm
+ p_priority = 0
+ p_priority = int(p_priority)
+ s.add((p_priority, p_owner))
+
+ res = [x[1] for x in sorted(s, key=lambda x: x[0])]
+ setattr(config, 'commit_scripts', res)
+
+ return res
+
class ConfigDiff(object):
"""
The class of config changes as represented by comparison between the
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index ccf2ce8f2..7d51b94e4 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -34,6 +34,9 @@ MIGRATE_LOAD_CONFIG = ['/usr/libexec/vyos/vyos-load-config.py']
SAVE_CONFIG = ['/usr/libexec/vyos/vyos-save-config.py']
INSTALL_IMAGE = ['/usr/libexec/vyos/op_mode/image_installer.py',
'--action', 'add', '--no-prompt', '--image-path']
+IMPORT_PKI = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'import']
+IMPORT_PKI_NO_PROMPT = ['/usr/libexec/vyos/op_mode/pki.py',
+ '--action', 'import', '--no-prompt']
REMOVE_IMAGE = ['/usr/libexec/vyos/op_mode/image_manager.py',
'--action', 'delete', '--no-prompt', '--image-name']
SET_DEFAULT_IMAGE = ['/usr/libexec/vyos/op_mode/image_manager.py',
@@ -239,6 +242,14 @@ class ConfigSession(object):
out = self.__run_command(REMOVE_IMAGE + [name])
return out
+ def import_pki(self, path):
+ out = self.__run_command(IMPORT_PKI + path)
+ return out
+
+ def import_pki_no_prompt(self, path):
+ out = self.__run_command(IMPORT_PKI_NO_PROMPT + path)
+ return out
+
def set_default_image(self, name):
out = self.__run_command(SET_DEFAULT_IMAGE + [name])
return out
diff --git a/python/vyos/pki.py b/python/vyos/pki.py
index 27fe793a8..5a0e2ddda 100644
--- a/python/vyos/pki.py
+++ b/python/vyos/pki.py
@@ -271,7 +271,7 @@ def load_private_key(raw_data, passphrase=None, wrap_tags=True):
try:
return serialization.load_pem_private_key(bytes(raw_data, 'utf-8'), password=passphrase)
- except ValueError:
+ except (ValueError, TypeError):
return False
def load_openssh_public_key(raw_data, type):
diff --git a/python/vyos/utils/dict.py b/python/vyos/utils/dict.py
index 1eb6abcd5..1a7a6b96f 100644
--- a/python/vyos/utils/dict.py
+++ b/python/vyos/utils/dict.py
@@ -267,6 +267,7 @@ def dict_to_paths_values(conf: dict) -> dict:
dict_of_options[path] = dict_search(path,conf)
return dict_of_options
+
def dict_to_key_paths(d: dict) -> list:
""" Generator to return list of key paths from dict of list[str]|str
"""
diff --git a/python/vyos/xml_ref/__init__.py b/python/vyos/xml_ref/__init__.py
index 91ce394f7..99d8432d2 100644
--- a/python/vyos/xml_ref/__init__.py
+++ b/python/vyos/xml_ref/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# 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
@@ -54,8 +54,8 @@ def is_valueless(path: list) -> bool:
def is_leaf(path: list) -> bool:
return load_reference().is_leaf(path)
-def owner(path: list) -> str:
- return load_reference().owner(path)
+def owner(path: list, with_tag=False) -> str:
+ return load_reference().owner(path, with_tag=with_tag)
def priority(path: list) -> str:
return load_reference().priority(path)
diff --git a/python/vyos/xml_ref/definition.py b/python/vyos/xml_ref/definition.py
index c85835ffd..5ff28daed 100644
--- a/python/vyos/xml_ref/definition.py
+++ b/python/vyos/xml_ref/definition.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# 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
@@ -139,28 +139,38 @@ class Xml:
ref_path = path.copy()
d = self.ref
data = ''
+ tag = ''
while ref_path and d:
+ tag_val = ''
d = d.get(ref_path[0], {})
ref_path.pop(0)
if self._is_tag_node(d) and ref_path:
+ tag_val = ref_path[0]
ref_path.pop(0)
if self._is_leaf_node(d) and ref_path:
ref_path.pop(0)
res = self._get_ref_node_data(d, name)
if res is not None:
data = res
+ tag = tag_val
- return data
+ return data, tag
- def owner(self, path: list) -> str:
+ def owner(self, path: list, with_tag=False) -> str:
from pathlib import Path
- data = self._least_upper_data(path, 'owner')
+ data, tag = self._least_upper_data(path, 'owner')
+ tag_ext = f'_{tag}' if tag else ''
if data:
- data = Path(data.split()[0]).name
+ if with_tag:
+ data = Path(data.split()[0]).stem
+ data = f'{data}{tag_ext}'
+ else:
+ data = Path(data.split()[0]).name
return data
def priority(self, path: list) -> str:
- return self._least_upper_data(path, 'priority')
+ data, _ = self._least_upper_data(path, 'priority')
+ return data
@staticmethod
def _dict_get(d: dict, path: list) -> dict:
diff --git a/python/vyos/xml_ref/generate_op_cache.py b/python/vyos/xml_ref/generate_op_cache.py
index e93b07974..cd2ac890e 100755
--- a/python/vyos/xml_ref/generate_op_cache.py
+++ b/python/vyos/xml_ref/generate_op_cache.py
@@ -13,15 +13,13 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#
import re
import sys
import json
import glob
+
from argparse import ArgumentParser
-from argparse import ArgumentTypeError
from os.path import join
from os.path import abspath
from os.path import dirname
diff --git a/smoketest/scripts/cli/test_config_dependency.py b/smoketest/scripts/cli/test_config_dependency.py
index 14e88321a..99e807ac5 100755
--- a/smoketest/scripts/cli/test_config_dependency.py
+++ b/smoketest/scripts/cli/test_config_dependency.py
@@ -16,13 +16,39 @@
import unittest
+from time import sleep
-from base_vyostest_shim import VyOSUnitTestSHIM
-
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.process import cmd
from vyos.configsession import ConfigSessionError
+from base_vyostest_shim import VyOSUnitTestSHIM
+
class TestConfigDep(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # smoketests are run without configd in 1.4; with configd in 1.5
+ # the tests below check behavior under configd:
+ # test_configdep_error checks for regression under configd (T6559)
+ # test_configdep_prio_queue checks resolution under configd (T6671)
+ cls.running_state = is_systemd_service_running('vyos-configd.service')
+
+ if not cls.running_state:
+ cmd('sudo systemctl start vyos-configd.service')
+ # allow time for init
+ sleep(1)
+
+ super(TestConfigDep, cls).setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestConfigDep, cls).tearDownClass()
+
+ # return to running_state
+ if not cls.running_state:
+ cmd('sudo systemctl stop vyos-configd.service')
+
def test_configdep_error(self):
address_group = 'AG'
address = '192.168.137.5'
@@ -45,5 +71,60 @@ class TestConfigDep(VyOSUnitTestSHIM.TestCase):
self.cli_delete(['nat'])
self.cli_commit()
+ def test_configdep_prio_queue(self):
+ # confirm that that a dependency (in this case, conntrack ->
+ # conntrack-sync) is not immediately called if the target is
+ # scheduled in the priority queue, indicating that it may require an
+ # intermediate activitation (bond0)
+ bonding_base = ['interfaces', 'bonding']
+ bond_interface = 'bond0'
+ bond_address = '192.0.2.1/24'
+ vrrp_group_base = ['high-availability', 'vrrp', 'group']
+ vrrp_sync_group_base = ['high-availability', 'vrrp', 'sync-group']
+ vrrp_group = 'ETH2'
+ vrrp_sync_group = 'GROUP'
+ conntrack_sync_base = ['service', 'conntrack-sync']
+ conntrack_peer = '192.0.2.77'
+
+ # simple set to trigger in-session conntrack -> conntrack-sync
+ # dependency; note that this is triggered on boot in 1.4 due to
+ # default 'system conntrack modules'
+ self.cli_set(['system', 'conntrack', 'table-size', '524288'])
+
+ self.cli_set(['interfaces', 'ethernet', 'eth2', 'address',
+ '198.51.100.2/24'])
+
+ self.cli_set(bonding_base + [bond_interface, 'address',
+ bond_address])
+ self.cli_set(bonding_base + [bond_interface, 'member', 'interface',
+ 'eth3'])
+
+ self.cli_set(vrrp_group_base + [vrrp_group, 'address',
+ '198.51.100.200/24'])
+ self.cli_set(vrrp_group_base + [vrrp_group, 'hello-source-address',
+ '198.51.100.2'])
+ self.cli_set(vrrp_group_base + [vrrp_group, 'interface', 'eth2'])
+ self.cli_set(vrrp_group_base + [vrrp_group, 'priority', '200'])
+ self.cli_set(vrrp_group_base + [vrrp_group, 'vrid', '22'])
+ self.cli_set(vrrp_sync_group_base + [vrrp_sync_group, 'member',
+ vrrp_group])
+
+ self.cli_set(conntrack_sync_base + ['failover-mechanism', 'vrrp',
+ 'sync-group', vrrp_sync_group])
+
+ self.cli_set(conntrack_sync_base + ['interface', bond_interface,
+ 'peer', conntrack_peer])
+
+ self.cli_commit()
+
+ # clean up
+ self.cli_delete(bonding_base)
+ self.cli_delete(vrrp_group_base)
+ self.cli_delete(vrrp_sync_group_base)
+ self.cli_delete(conntrack_sync_base)
+ self.cli_delete(['interfaces', 'ethernet', 'eth2', 'address'])
+ self.cli_delete(['system', 'conntrack', 'table-size'])
+ self.cli_commit()
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
index d8a091aaa..e087b8735 100755
--- a/smoketest/scripts/cli/test_interfaces_openvpn.py
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -250,6 +250,67 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
interface = f'vtun{ii}'
self.assertNotIn(interface, interfaces())
+ def test_openvpn_client_ip_version(self):
+ # Test the client mode behavior combined with different IP protocol versions
+
+ interface = 'vtun10'
+ remote_host = '192.0.2.10'
+ remote_host_v6 = 'fd00::2:10'
+ path = base_path + [interface]
+ auth_hash = 'sha1'
+
+ # Default behavior: client uses uspecified protocol version (udp)
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes256'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'client'])
+ self.cli_set(path + ['persistent-tunnel'])
+ self.cli_set(path + ['protocol', 'udp'])
+ self.cli_set(path + ['remote-host', remote_host])
+ self.cli_set(path + ['remote-port', remote_port])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['vrf', vrf_name])
+ self.cli_set(path + ['authentication', 'username', interface+'user'])
+ self.cli_set(path + ['authentication', 'password', interface+'secretpw'])
+
+ self.cli_commit()
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev vtun10', config)
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'persist-key', config)
+ self.assertIn(f'proto udp', config)
+ self.assertIn(f'rport {remote_port}', config)
+ self.assertIn(f'remote {remote_host}', config)
+ self.assertIn(f'persist-tun', config)
+
+ # IPv4 only: client usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: client uses udp6 protocol
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_delete(path + ['remote-host', remote_host])
+ self.cli_set(path + ['remote-host', remote_host_v6])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+
+ # IPv6 dual-stack: not allowed in client mode
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
def test_openvpn_server_verify(self):
# Create one OpenVPN server interface and check required verify() stages
interface = 'vtun5000'
@@ -453,6 +514,74 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
interface = f'vtun{ii}'
self.assertNotIn(interface, interfaces())
+ def test_openvpn_server_ip_version(self):
+ # Test the server mode behavior combined with each IP protocol version
+
+ auth_hash = 'sha256'
+ port = '2000'
+
+ interface = 'vtun20'
+ subnet = '192.0.20.0/24'
+ path = base_path + [interface]
+
+ # Default behavior: client uses uspecified protocol version (udp)
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'server'])
+ self.cli_set(path + ['local-port', port])
+ self.cli_set(path + ['server', 'subnet', subnet])
+ self.cli_set(path + ['server', 'topology', 'subnet'])
+
+ self.cli_set(path + ['replace-default-route'])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+
+ self.cli_commit()
+
+ start_addr = inc_ip(subnet, '2')
+ stop_addr = last_host_address(subnet)
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'persist-key', config)
+ self.assertIn(f'proto udp', config) # default protocol
+ self.assertIn(f'auth {auth_hash}', config)
+ self.assertIn(f'data-ciphers AES-192-CBC', config)
+ self.assertIn(f'topology subnet', config)
+ self.assertIn(f'lport {port}', config)
+ self.assertIn(f'push "redirect-gateway def1"', config)
+
+ # IPv4 only: server usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: server uses udp6 protocol + bind ipv6only
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertIn(f'bind ipv6only', config)
+
+ # IPv6 dual-stack: server uses udp6 protocol without bind ipv6only
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertNotIn(f'bind ipv6only', config)
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
def test_openvpn_site2site_verify(self):
# Create one OpenVPN site2site interface and check required
# verify() stages
@@ -627,6 +756,63 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(interface, interfaces())
+ def test_openvpn_site2site_ip_version(self):
+ # Test the site-to-site mode behavior combined with each IP protocol version
+
+ encryption_cipher = 'aes256'
+
+ interface = 'vtun30'
+ local_address = '192.0.30.1'
+ local_address_subnet = '255.255.255.252'
+ remote_address = '172.16.30.1'
+ path = base_path + [interface]
+ port = '3030'
+
+ self.cli_set(path + ['local-address', local_address])
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['mode', 'site-to-site'])
+ self.cli_set(path + ['local-port', port])
+ self.cli_set(path + ['remote-port', port])
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
+ self.cli_set(path + ['remote-address', remote_address])
+ self.cli_set(path + ['encryption', 'cipher', encryption_cipher])
+
+ self.cli_commit()
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'ifconfig {local_address} {remote_address}', config)
+ self.assertIn(f'proto udp', config)
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'secret /run/openvpn/{interface}_shared.key', config)
+ self.assertIn(f'lport {port}', config)
+ self.assertIn(f'rport {port}', config)
+
+ # IPv4 only: server usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: server uses udp6 protocol + bind ipv6only
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertIn(f'bind ipv6only', config)
+
+ # IPv6 dual-stack: not allowed in site-to-site mode
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
def test_openvpn_server_server_bridge(self):
# Create OpenVPN server interface using bridge.
# Validate configuration afterwards.
diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py
index d1ff25a58..6dbb6add4 100755
--- a/smoketest/scripts/cli/test_service_router-advert.py
+++ b/smoketest/scripts/cli/test_service_router-advert.py
@@ -224,5 +224,34 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
self.assertIn(tmp, config)
self.assertIn('AdvValidLifetime 65528;', config) # default
+ def test_advsendadvert_advintervalopt(self):
+ ra_src = ['fe80::1', 'fe80::2']
+
+ self.cli_set(base_path + ['prefix', prefix])
+ self.cli_set(base_path + ['no-send-advert'])
+ # commit changes
+ self.cli_commit()
+
+ # Verify generated configuration
+ config = read_file(RADVD_CONF)
+ tmp = get_config_value('AdvSendAdvert')
+ self.assertEqual(tmp, 'off')
+
+ tmp = get_config_value('AdvIntervalOpt')
+ self.assertEqual(tmp, 'on')
+
+ self.cli_set(base_path + ['no-send-interval'])
+ # commit changes
+ self.cli_commit()
+
+ # Verify generated configuration
+ config = read_file(RADVD_CONF)
+ tmp = get_config_value('AdvSendAdvert')
+ self.assertEqual(tmp, 'off')
+
+ tmp = get_config_value('AdvIntervalOpt')
+ self.assertEqual(tmp, 'off')
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_option.py b/smoketest/scripts/cli/test_system_option.py
index c6f48bfc6..ffb1d76ae 100755
--- a/smoketest/scripts/cli/test_system_option.py
+++ b/smoketest/scripts/cli/test_system_option.py
@@ -80,5 +80,20 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase):
self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh2'), gc_thresh2)
self.assertEqual(sysctl_read('net.ipv4.neigh.default.gc_thresh3'), gc_thresh3)
+ def test_ssh_client_options(self):
+ loopback = 'lo'
+ ssh_client_opt_file = '/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf'
+
+ self.cli_set(['system', 'option', 'ssh-client', 'source-interface', loopback])
+ self.cli_commit()
+
+ tmp = read_file(ssh_client_opt_file)
+ self.assertEqual(tmp, f'BindInterface {loopback}')
+
+ self.cli_delete(['system', 'option'])
+ self.cli_commit()
+ self.assertFalse(os.path.exists(ssh_client_opt_file))
+
+
if __name__ == '__main__':
unittest.main(verbosity=2, failfast=True)
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index 9105ce1f8..8c1213e2b 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -123,6 +123,18 @@ def get_config(config=None):
openvpn['module_load_dco'] = {}
break
+ # Calculate the protocol modifier. This is concatenated to the protocol string to direct
+ # OpenVPN to use a specific IP protocol version. If unspecified, the kernel decides which
+ # type of socket to open. In server mode, an additional "ipv6-dual-stack" option forces
+ # binding the socket in IPv6 mode, which can also receive IPv4 traffic (when using the
+ # default "ipv6" mode, we specify "bind ipv6only" to disable kernel dual-stack behaviors).
+ if openvpn['ip_version'] == 'ipv4':
+ openvpn['protocol_modifier'] = '4'
+ elif openvpn['ip_version'] in ['ipv6', 'dual-stack']:
+ openvpn['protocol_modifier'] = '6'
+ else:
+ openvpn['protocol_modifier'] = ''
+
return openvpn
def is_ec_private_key(pki, cert_name):
@@ -257,6 +269,9 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
+ if 'ip_version' in openvpn and openvpn['ip_version'] == 'dual-stack':
+ raise ConfigError('"ip-version dual-stack" is not supported in client mode')
+
if dict_search('tls.dh_params', openvpn):
raise ConfigError('Cannot specify "tls dh-params" in client mode')
@@ -264,6 +279,9 @@ def verify(openvpn):
# OpenVPN site-to-site - VERIFY
#
elif openvpn['mode'] == 'site-to-site':
+ if 'ip_version' in openvpn and openvpn['ip_version'] == 'dual-stack':
+ raise ConfigError('"ip-version dual-stack" is not supported in site-to-site mode')
+
if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "local-address" or add interface to bridge')
@@ -487,6 +505,25 @@ def verify(openvpn):
# not depending on any operation mode
#
+ # verify that local_host/remote_host match with any ip_version override
+ # specified (if a dns name is specified for remote_host, no attempt is made
+ # to verify that record resolves to an address of the configured family)
+ if 'local_host' in openvpn:
+ if openvpn['ip_version'] == 'ipv4' and is_ipv6(openvpn['local_host']):
+ raise ConfigError('Cannot use an IPv6 "local-host" with "ip-version ipv4"')
+ elif openvpn['ip_version'] == 'ipv6' and is_ipv4(openvpn['local_host']):
+ raise ConfigError('Cannot use an IPv4 "local-host" with "ip-version ipv6"')
+ elif openvpn['ip_version'] == 'dual-stack':
+ raise ConfigError('Cannot use "local-host" with "ip-version dual-stack". "dual-stack" is only supported when OpenVPN binds to all available interfaces.')
+
+ if 'remote_host' in openvpn:
+ remote_hosts = dict_search('remote_host', openvpn)
+ for remote_host in remote_hosts:
+ if openvpn['ip_version'] == 'ipv4' and is_ipv6(remote_host):
+ raise ConfigError('Cannot use an IPv6 "remote-host" with "ip-version ipv4"')
+ elif openvpn['ip_version'] == 'ipv6' and is_ipv4(remote_host):
+ raise ConfigError('Cannot use an IPv4 "remote-host" with "ip-version ipv6"')
+
# verify specified IP address is present on any interface on this system
if 'local_host' in openvpn:
if not is_addr_assigned(openvpn['local_host']):
diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py
index d1647e3a1..52d0b7cda 100755
--- a/src/conf_mode/system_option.py
+++ b/src/conf_mode/system_option.py
@@ -85,6 +85,8 @@ def verify(options):
raise ConfigError('No interface with address "{address}" configured!')
if 'source_interface' in config:
+ # verify_source_interface reuires key 'ifname'
+ config['ifname'] = config['source_interface']
verify_source_interface(config)
if 'source_address' in config:
address = config['source_address']
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
index 5d879471d..2a1c5a7b2 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -72,6 +72,22 @@ function delroute () {
fi
}
+# try to communicate with vtysh
+function vtysh_conf () {
+ # perform 10 attempts with 1 second delay for retries
+ for i in {1..10} ; do
+ if vtysh -c "conf t" -c "$1" ; then
+ logmsg info "Command was executed successfully via vtysh: \"$1\""
+ return 0
+ else
+ logmsg info "Failed to send command to vtysh, retrying in 1 second"
+ sleep 1
+ fi
+ done
+ logmsg error "Failed to execute command via vtysh after 10 attempts: \"$1\""
+ return 1
+}
+
# replace ip command with this wrapper
function ip () {
# pass comand to system `ip` if this is not related to routes change
@@ -84,7 +100,7 @@ function ip () {
delroute ${@:4}
iptovtysh $@
logmsg info "Sending command to vtysh"
- vtysh -c "conf t" -c "$VTYSH_CMD"
+ vtysh_conf "$VTYSH_CMD"
else
# add ip route to kernel
logmsg info "Modifying routes in kernel: \"/usr/sbin/ip $@\""
diff --git a/src/op_mode/ntp.py b/src/op_mode/ntp.py
index e14cc46d0..6ec0fedcb 100644
--- a/src/op_mode/ntp.py
+++ b/src/op_mode/ntp.py
@@ -110,49 +110,62 @@ def _is_configured():
if not config.exists("service ntp"):
raise vyos.opmode.UnconfiguredSubsystem("NTP service is not enabled.")
+def _extend_command_vrf():
+ config = ConfigTreeQuery()
+ if config.exists('service ntp vrf'):
+ vrf = config.value('service ntp vrf')
+ return f'ip vrf exec {vrf} '
+ return ''
+
+
def show_activity(raw: bool):
_is_configured()
command = f'chronyc'
if raw:
- command += f" -c activity"
- return _get_raw_data(command)
+ command += f" -c activity"
+ return _get_raw_data(command)
else:
- command += f" activity"
- return cmd(command)
+ command = _extend_command_vrf() + command
+ command += f" activity"
+ return cmd(command)
def show_sources(raw: bool):
_is_configured()
command = f'chronyc'
if raw:
- command += f" -c sources"
- return _get_raw_data(command)
+ command += f" -c sources"
+ return _get_raw_data(command)
else:
- command += f" sources -v"
- return cmd(command)
+ command = _extend_command_vrf() + command
+ command += f" sources -v"
+ return cmd(command)
def show_tracking(raw: bool):
_is_configured()
command = f'chronyc'
if raw:
- command += f" -c tracking"
- return _get_raw_data(command)
+ command += f" -c tracking"
+ return _get_raw_data(command)
else:
- command += f" tracking"
- return cmd(command)
+ command = _extend_command_vrf() + command
+ command += f" tracking"
+ return cmd(command)
def show_sourcestats(raw: bool):
_is_configured()
command = f'chronyc'
if raw:
- command += f" -c sourcestats"
- return _get_raw_data(command)
+ command += f" -c sourcestats"
+ return _get_raw_data(command)
else:
- command += f" sourcestats -v"
- return cmd(command)
+ command = _extend_command_vrf() + command
+ command += f" sourcestats -v"
+ return cmd(command)
+
if __name__ == '__main__':
try:
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 84b080023..ab613e5c4 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -316,7 +316,13 @@ def generate_certificate_request(private_key=None, key_type=None, return_request
default_values = get_default_values()
subject = {}
- subject['country'] = ask_input('Enter country code:', default=default_values['country'])
+ while True:
+ country = ask_input('Enter country code:', default=default_values['country'])
+ if len(country) != 2:
+ print("Country name must be a 2 character country code")
+ continue
+ subject['country'] = country
+ break
subject['state'] = ask_input('Enter state:', default=default_values['state'])
subject['locality'] = ask_input('Enter locality:', default=default_values['locality'])
subject['organization'] = ask_input('Enter organization name:', default=default_values['organization'])
@@ -693,7 +699,7 @@ def generate_wireguard_psk(interface=None, peer=None, install=False):
print(f'Pre-shared key: {psk}')
# Import functions
-def import_ca_certificate(name, path=None, key_path=None):
+def import_ca_certificate(name, path=None, key_path=None, no_prompt=False, passphrase=None):
if path:
if not os.path.exists(path):
print(f'File not found: {path}')
@@ -717,19 +723,20 @@ def import_ca_certificate(name, path=None, key_path=None):
return
key = None
- passphrase = ask_input('Enter private key passphrase: ') or None
+ if not no_prompt:
+ passphrase = ask_input('Enter private key passphrase: ') or None
with open(key_path) as f:
key_data = f.read()
key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False)
if not key:
- print(f'Invalid private key or passphrase: {path}')
+ print(f'Invalid private key or passphrase: {key_path}')
return
install_certificate(name, private_key=key, is_ca=True)
-def import_certificate(name, path=None, key_path=None):
+def import_certificate(name, path=None, key_path=None, no_prompt=False, passphrase=None):
if path:
if not os.path.exists(path):
print(f'File not found: {path}')
@@ -753,14 +760,15 @@ def import_certificate(name, path=None, key_path=None):
return
key = None
- passphrase = ask_input('Enter private key passphrase: ') or None
+ if not no_prompt:
+ passphrase = ask_input('Enter private key passphrase: ') or None
with open(key_path) as f:
key_data = f.read()
key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False)
if not key:
- print(f'Invalid private key or passphrase: {path}')
+ print(f'Invalid private key or passphrase: {key_path}')
return
install_certificate(name, private_key=key, is_ca=False)
@@ -799,7 +807,7 @@ def import_dh_parameters(name, path):
install_dh_parameters(name, dh)
-def import_keypair(name, path=None, key_path=None):
+def import_keypair(name, path=None, key_path=None, no_prompt=False, passphrase=None):
if path:
if not os.path.exists(path):
print(f'File not found: {path}')
@@ -823,14 +831,15 @@ def import_keypair(name, path=None, key_path=None):
return
key = None
- passphrase = ask_input('Enter private key passphrase: ') or None
+ if not no_prompt:
+ passphrase = ask_input('Enter private key passphrase: ') or None
with open(key_path) as f:
key_data = f.read()
key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False)
if not key:
- print(f'Invalid private key or passphrase: {path}')
+ print(f'Invalid private key or passphrase: {key_path}')
return
install_keypair(name, None, private_key=key, prompt=False)
@@ -1011,6 +1020,9 @@ if __name__ == '__main__':
parser.add_argument('--filename', help='Write certificate into specified filename', action='store')
parser.add_argument('--key-filename', help='Write key into specified filename', action='store')
+ parser.add_argument('--no-prompt', action='store_true', help='Perform action non-interactively')
+ parser.add_argument('--passphrase', help='A passphrase to decrypt the private key')
+
args = parser.parse_args()
try:
@@ -1054,15 +1066,18 @@ if __name__ == '__main__':
generate_wireguard_psk(args.interface, peer=args.peer, install=args.install)
elif args.action == 'import':
if args.ca:
- import_ca_certificate(args.ca, path=args.filename, key_path=args.key_filename)
+ import_ca_certificate(args.ca, path=args.filename, key_path=args.key_filename,
+ no_prompt=args.no_prompt, passphrase=args.passphrase)
elif args.certificate:
- import_certificate(args.certificate, path=args.filename, key_path=args.key_filename)
+ import_certificate(args.certificate, path=args.filename, key_path=args.key_filename,
+ no_prompt=args.no_prompt, passphrase=args.passphrase)
elif args.crl:
import_crl(args.crl, args.filename)
elif args.dh:
import_dh_parameters(args.dh, args.filename)
elif args.keypair:
- import_keypair(args.keypair, path=args.filename, key_path=args.key_filename)
+ import_keypair(args.keypair, path=args.filename, key_path=args.key_filename,
+ no_prompt=args.no_prompt, passphrase=args.passphrase)
elif args.openvpn:
import_openvpn_secret(args.openvpn, args.filename)
elif args.action == 'show':
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index d797e90cf..3674d9627 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -30,6 +30,7 @@ from vyos.defaults import directories
from vyos.utils.boot import boot_configuration_complete
from vyos.configsource import ConfigSourceString
from vyos.configsource import ConfigSourceError
+from vyos.configdiff import get_commit_scripts
from vyos.config import Config
from vyos import ConfigError
@@ -220,6 +221,12 @@ def initialization(socket):
dependent_func: dict[str, list[typing.Callable]] = {}
setattr(config, 'dependent_func', dependent_func)
+ commit_scripts = get_commit_scripts(config)
+ logger.debug(f'commit_scripts: {commit_scripts}')
+
+ scripts_called = []
+ setattr(config, 'scripts_called', scripts_called)
+
return config
def process_node_data(config, data, last: bool = False) -> int:
@@ -228,6 +235,7 @@ def process_node_data(config, data, last: bool = False) -> int:
return R_ERROR_DAEMON
script_name = None
+ os.environ['VYOS_TAGNODE_VALUE'] = ''
args = []
config.dependency_list.clear()
@@ -244,6 +252,12 @@ def process_node_data(config, data, last: bool = False) -> int:
args = res.group(3).split()
args.insert(0, f'{script_name}.py')
+ tag_value = os.getenv('VYOS_TAGNODE_VALUE', '')
+ tag_ext = f'_{tag_value}' if tag_value else ''
+ script_record = f'{script_name}{tag_ext}'
+ scripts_called = getattr(config, 'scripts_called', [])
+ scripts_called.append(script_record)
+
if script_name not in include_set:
return R_PASS
@@ -302,11 +316,12 @@ if __name__ == '__main__':
socket.send(resp.encode())
config = initialization(socket)
elif message["type"] == "node":
- if message["last"]:
- logger.debug(f'final element of priority queue')
res = process_node_data(config, message["data"], message["last"])
response = res.to_bytes(1, byteorder=sys.byteorder)
logger.debug(f"Sending response {res}")
socket.send(response)
+ if message["last"] and config:
+ scripts_called = getattr(config, 'scripts_called', [])
+ logger.debug(f'scripts_called: {scripts_called}')
else:
logger.critical(f"Unexpected message: {message}")
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 7f5233c6b..97633577d 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -212,6 +212,22 @@ class ImageModel(ApiModel):
}
}
+class ImportPkiModel(ApiModel):
+ op: StrictStr
+ path: List[StrictStr]
+ passphrase: StrictStr = None
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "op": "import_pki",
+ "path": ["op", "mode", "path"],
+ "passphrase": "passphrase",
+ }
+ }
+
+
class ContainerImageModel(ApiModel):
op: StrictStr
name: StrictStr = None
@@ -585,6 +601,14 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
return success(msg)
+def create_path_import_pki_no_prompt(path):
+ correct_paths = ['ca', 'certificate', 'key-pair']
+ if path[1] not in correct_paths:
+ return False
+ path[1] = '--' + path[1].replace('-', '')
+ path[3] = '--key-filename'
+ return path[1:]
+
@app.post('/configure')
def configure_op(data: Union[ConfigureModel,
ConfigureListModel],
@@ -814,6 +838,44 @@ def reset_op(data: ResetModel):
return success(res)
+@app.post('/import-pki')
+def import_pki(data: ImportPkiModel):
+ session = app.state.vyos_session
+
+ op = data.op
+ path = data.path
+
+ lock.acquire()
+
+ try:
+ if op == 'import-pki':
+ # need to get rid or interactive mode for private key
+ if len(path) == 5 and path[3] in ['key-file', 'private-key']:
+ path_no_prompt = create_path_import_pki_no_prompt(path)
+ if not path_no_prompt:
+ return error(400, f"Invalid command: {' '.join(path)}")
+ if data.passphrase:
+ path_no_prompt += ['--passphrase', data.passphrase]
+ res = session.import_pki_no_prompt(path_no_prompt)
+ else:
+ res = session.import_pki(path)
+ if not res[0].isdigit():
+ return error(400, res)
+ # commit changes
+ session.commit()
+ res = res.split('. ')[0]
+ else:
+ return error(400, f"'{op}' is not a valid operation")
+ except ConfigSessionError as e:
+ return error(400, str(e))
+ except Exception as e:
+ logger.critical(traceback.format_exc())
+ return error(500, "An internal error occured. Check the logs for details.")
+ finally:
+ lock.release()
+
+ return success(res)
+
@app.post('/poweroff')
def poweroff_op(data: PoweroffModel):
session = app.state.vyos_session