summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/package-smoketest.yml41
-rw-r--r--debian/control3
-rw-r--r--debian/vyos-1x.install1
-rw-r--r--interface-definitions/include/firewall/set-packet-modifications-table-and-vrf.xml.i19
-rw-r--r--interface-definitions/include/firewall/vrf.xml.i20
-rw-r--r--interface-definitions/policy_local-route.xml.in2
-rw-r--r--interface-definitions/system_option.xml.in8
-rw-r--r--python/vyos/configtree.py6
-rw-r--r--python/vyos/utils/config.py66
-rw-r--r--python/vyos/xml_ref/definition.py28
-rw-r--r--smoketest/scripts/cli/base_accel_ppp_test.py6
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py12
-rw-r--r--smoketest/scripts/cli/test_policy_local-route.py171
-rwxr-xr-xsmoketest/scripts/cli/test_policy_route.py15
-rwxr-xr-xsmoketest/scripts/cli/test_system_option.py2
-rwxr-xr-xsrc/conf_mode/pki.py33
-rwxr-xr-xsrc/conf_mode/policy_local-route.py45
-rwxr-xr-xsrc/conf_mode/protocols_static.py2
-rw-r--r--src/op_mode/mtr.py14
-rwxr-xr-xsrc/op_mode/pki.py17
-rwxr-xr-xsrc/utils/vyos-commands-to-config53
21 files changed, 503 insertions, 61 deletions
diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml
index 3595bcf0e..91c968c82 100644
--- a/.github/workflows/package-smoketest.yml
+++ b/.github/workflows/package-smoketest.yml
@@ -83,12 +83,43 @@ jobs:
with:
name: vyos-${{ needs.build_iso.outputs.build_version }}
path: build
- - name: VyOS CLI smoketests
+ - name: VyOS CLI smoketests (no interfaces)
id: test
shell: bash
run: |
set -e
- sudo make test
+ sudo make test-no-interfaces
+ if [[ $? == 0 ]]; then
+ echo "exit_code=success" >> $GITHUB_OUTPUT
+ else
+ echo "exit_code=fail" >> $GITHUB_OUTPUT
+ fi
+
+ test_interfaces_cli:
+ needs: build_iso
+ runs-on: ubuntu-24.04
+ timeout-minutes: 180
+ container:
+ image: vyos/vyos-build:current
+ options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged
+ outputs:
+ exit_code: ${{ steps.test.outputs.exit_code }}
+ steps:
+ # We need the test script from vyos-build repo
+ - name: Clone vyos-build source code
+ uses: actions/checkout@v4
+ with:
+ repository: vyos/vyos-build
+ - uses: actions/download-artifact@v4
+ with:
+ name: vyos-${{ needs.build_iso.outputs.build_version }}
+ path: build
+ - name: VyOS CLI smoketests (interfaces only)
+ id: test
+ shell: bash
+ run: |
+ set -e
+ sudo make test-interfaces
if [[ $? == 0 ]]; then
echo "exit_code=success" >> $GITHUB_OUTPUT
else
@@ -191,6 +222,7 @@ jobs:
result:
needs:
- test_smoketest_cli
+ - test_interfaces_cli
- test_config_load
- test_raid1_install
- test_encrypted_config_tpm
@@ -203,13 +235,14 @@ jobs:
uses: mshick/add-pr-comment@v2
with:
message: |
- CI integration ${{ needs.test_smoketest_cli.outputs.exit_code == 'success' && needs.test_config_load.outputs.exit_code == 'success' && needs.test_raid1_install.outputs.exit_code == 'success' && '👍 passed!' || '❌ failed!' }}
+ CI integration ${{ needs.test_smoketest_cli.outputs.exit_code == 'success' && needs.test_interfaces_cli.outputs.exit_code == 'success' && needs.test_config_load.outputs.exit_code == 'success' && needs.test_raid1_install.outputs.exit_code == 'success' && '👍 passed!' || '❌ failed!' }}
### Details
[CI logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
- * CLI Smoketests ${{ needs.test_smoketest_cli.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
+ * CLI Smoketests (no interfaces) ${{ needs.test_smoketest_cli.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
+ * CLI Smoketests (interfaces only) ${{ needs.test_interfaces_cli.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
* Config tests ${{ needs.test_config_load.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
* RAID1 tests ${{ needs.test_raid1_install.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
* TPM tests ${{ needs.test_encrypted_config_tpm.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
diff --git a/debian/control b/debian/control
index f8cfb876c..15fb5d72e 100644
--- a/debian/control
+++ b/debian/control
@@ -235,6 +235,9 @@ Depends:
squidclient,
squidguard,
# End "service webproxy"
+# For "service monitoring node-exporter"
+ node-exporter,
+# End "service monitoring node-exporter"
# For "service monitoring telegraf"
telegraf (>= 1.20),
# End "service monitoring telegraf"
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index fff6ebeab..502fc7aaa 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -29,6 +29,7 @@ usr/bin/vyos-show-config
usr/bin/vyos-config-file-query
usr/bin/vyos-config-to-commands
usr/bin/vyos-config-to-json
+usr/bin/vyos-commands-to-config
usr/bin/vyos-hostsd-client
usr/lib
usr/libexec/vyos/activate
diff --git a/interface-definitions/include/firewall/set-packet-modifications-table-and-vrf.xml.i b/interface-definitions/include/firewall/set-packet-modifications-table-and-vrf.xml.i
index c7875b31d..5eb1984a5 100644
--- a/interface-definitions/include/firewall/set-packet-modifications-table-and-vrf.xml.i
+++ b/interface-definitions/include/firewall/set-packet-modifications-table-and-vrf.xml.i
@@ -25,24 +25,7 @@
</completionHelp>
</properties>
</leafNode>
- <leafNode name="vrf">
- <properties>
- <help>VRF to forward packet with</help>
- <valueHelp>
- <format>txt</format>
- <description>VRF instance name</description>
- </valueHelp>
- <valueHelp>
- <format>default</format>
- <description>Forward into default global VRF</description>
- </valueHelp>
- <completionHelp>
- <list>default</list>
- <path>vrf name</path>
- </completionHelp>
- #include <include/constraint/vrf.xml.i>
- </properties>
- </leafNode>
+ #include <include/firewall/vrf.xml.i>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/include/firewall/vrf.xml.i b/interface-definitions/include/firewall/vrf.xml.i
new file mode 100644
index 000000000..af8ce3ab4
--- /dev/null
+++ b/interface-definitions/include/firewall/vrf.xml.i
@@ -0,0 +1,20 @@
+<!-- include start from firewall/vrf.xml.i -->
+<leafNode name="vrf">
+ <properties>
+ <help>VRF to forward packet with</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>VRF instance name</description>
+ </valueHelp>
+ <valueHelp>
+ <format>default</format>
+ <description>Forward into default global VRF</description>
+ </valueHelp>
+ <completionHelp>
+ <list>default</list>
+ <path>vrf name</path>
+ </completionHelp>
+ #include <include/constraint/vrf.xml.i>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/policy_local-route.xml.in b/interface-definitions/policy_local-route.xml.in
index 7a019154a..9f6588db8 100644
--- a/interface-definitions/policy_local-route.xml.in
+++ b/interface-definitions/policy_local-route.xml.in
@@ -39,6 +39,7 @@
</completionHelp>
</properties>
</leafNode>
+ #include <include/firewall/vrf.xml.i>
</children>
</node>
<leafNode name="fwmark">
@@ -113,6 +114,7 @@
</completionHelp>
</properties>
</leafNode>
+ #include <include/firewall/vrf.xml.i>
</children>
</node>
<leafNode name="fwmark">
diff --git a/interface-definitions/system_option.xml.in b/interface-definitions/system_option.xml.in
index dc9958ff5..064d9ff40 100644
--- a/interface-definitions/system_option.xml.in
+++ b/interface-definitions/system_option.xml.in
@@ -88,7 +88,7 @@
<properties>
<help>System keyboard layout, type ISO2</help>
<completionHelp>
- <list>us uk fr de es fi jp106 no dk se-latin1 dvorak</list>
+ <list>us uk fr de es fi it jp106 no dk se-latin1 dvorak</list>
</completionHelp>
<valueHelp>
<format>us</format>
@@ -115,6 +115,10 @@
<description>Finland</description>
</valueHelp>
<valueHelp>
+ <format>it</format>
+ <description>Italy</description>
+ </valueHelp>
+ <valueHelp>
<format>jp106</format>
<description>Japan</description>
</valueHelp>
@@ -135,7 +139,7 @@
<description>Dvorak</description>
</valueHelp>
<constraint>
- <regex>(us|uk|fr|de|es|fi|jp106|no|dk|se-latin1|dvorak)</regex>
+ <regex>(us|uk|fr|de|es|fi|it|jp106|no|dk|se-latin1|dvorak)</regex>
</constraint>
<constraintErrorMessage>Invalid keyboard layout</constraintErrorMessage>
</properties>
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index ee8ca8b83..3e02fbba6 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -141,7 +141,7 @@ class ConfigTree(object):
self.__is_tag.restype = c_int
self.__set_tag = self.__lib.set_tag
- self.__set_tag.argtypes = [c_void_p, c_char_p]
+ self.__set_tag.argtypes = [c_void_p, c_char_p, c_bool]
self.__set_tag.restype = c_int
self.__is_leaf = self.__lib.is_leaf
@@ -359,11 +359,11 @@ class ConfigTree(object):
else:
return False
- def set_tag(self, path):
+ def set_tag(self, path, value=True):
check_path(path)
path_str = " ".join(map(str, path)).encode()
- res = self.__set_tag(self.__config, path_str)
+ res = self.__set_tag(self.__config, path_str, value)
if (res == 0):
return True
else:
diff --git a/python/vyos/utils/config.py b/python/vyos/utils/config.py
index 33047010b..deda13c13 100644
--- a/python/vyos/utils/config.py
+++ b/python/vyos/utils/config.py
@@ -14,8 +14,14 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+from typing import TYPE_CHECKING
+
from vyos.defaults import directories
+# https://peps.python.org/pep-0484/#forward-references
+if TYPE_CHECKING:
+ from vyos.configtree import ConfigTree
+
config_file = os.path.join(directories['config'], 'config.boot')
def read_saved_value(path: list):
@@ -37,3 +43,63 @@ def read_saved_value(path: list):
if len(res) == 1:
return ' '.join(res)
return res
+
+def flag(l: list) -> list:
+ res = [l[0:i] for i,_ in enumerate(l, start=1)]
+ return res
+
+def tag_node_of_path(p: list) -> list:
+ from vyos.xml_ref import is_tag
+
+ fl = flag(p)
+ res = list(map(is_tag, fl))
+
+ return res
+
+def set_tags(ct: 'ConfigTree', path: list) -> None:
+ fl = flag(path)
+ if_tag = tag_node_of_path(path)
+ for condition, target in zip(if_tag, fl):
+ if condition:
+ ct.set_tag(target)
+
+def parse_commands(cmds: str) -> dict:
+ from re import split as re_split
+ from shlex import split as shlex_split
+
+ from vyos.xml_ref import definition
+ from vyos.xml_ref.pkg_cache.vyos_1x_cache import reference
+
+ ref_tree = definition.Xml()
+ ref_tree.define(reference)
+
+ res = []
+
+ cmds = re_split(r'\n+', cmds)
+ for c in cmds:
+ cmd_parts = shlex_split(c)
+
+ if not cmd_parts:
+ # Ignore empty lines
+ continue
+
+ path = cmd_parts[1:]
+ op = cmd_parts[0]
+
+ try:
+ path, value = ref_tree.split_path(path)
+ except ValueError as e:
+ raise ValueError(f'Incorrect command: {e}')
+
+ entry = {}
+ entry["op"] = op
+ entry["path"] = path
+ entry["value"] = value
+
+ entry["is_multi"] = ref_tree.is_multi(path)
+ entry["is_leaf"] = ref_tree.is_leaf(path)
+ entry["is_tag"] = ref_tree.is_tag(path)
+
+ res.append(entry)
+
+ return res
diff --git a/python/vyos/xml_ref/definition.py b/python/vyos/xml_ref/definition.py
index 5ff28daed..4e755ab72 100644
--- a/python/vyos/xml_ref/definition.py
+++ b/python/vyos/xml_ref/definition.py
@@ -13,7 +13,7 @@
# 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 typing import Optional, Union, Any, TYPE_CHECKING
+from typing import Tuple, Optional, Union, Any, TYPE_CHECKING
# https://peps.python.org/pep-0484/#forward-references
# for type 'ConfigDict'
@@ -90,6 +90,32 @@ class Xml:
res = self._get_ref_node_data(node, 'node_type')
return res == 'tag'
+ def exists(self, path: list) -> bool:
+ try:
+ _ = self._get_ref_path(path)
+ return True
+ except ValueError:
+ return False
+
+ def split_path(self, path: list) -> Tuple[list, Optional[str]]:
+ """ Splits a list into config path and value components """
+
+ # First, check if the complete path is valid by itself
+ if self.exists(path):
+ if self.is_valueless(path) or not self.is_leaf(path):
+ # It's a complete path for a valueless node
+ # or a path to an empy non-leaf node
+ return (path, None)
+ else:
+ raise ValueError(f'Path "{path}" needs a value or children')
+ else:
+ # If the complete path doesn't exist, it's probably a path with a value
+ if self.exists(path[0:-1]):
+ return (path[0:-1], path[-1])
+ else:
+ # Or not a valid path at all
+ raise ValueError(f'Path "{path}" is incorrect')
+
def is_tag(self, path: list) -> bool:
ref_path = path.copy()
d = self.ref
diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py
index c6f6cb804..750702e98 100644
--- a/smoketest/scripts/cli/base_accel_ppp_test.py
+++ b/smoketest/scripts/cli/base_accel_ppp_test.py
@@ -14,6 +14,7 @@
import re
+from time import sleep
from base_vyostest_shim import VyOSUnitTestSHIM
from configparser import ConfigParser
@@ -641,6 +642,11 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}"""
for log_level in range(0, 5):
self.set(['log', 'level', str(log_level)])
self.cli_commit()
+
+ # Systemd comes with a default of 5 restarts in 10 seconds policy,
+ # this limit can be hit by this reastart sequence, slow down a bit
+ sleep(5)
+
# Validate configuration values
conf = ConfigParser(allow_no_value=True)
conf.read(self._config_file)
diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py
index 940306ac3..a383e596c 100644
--- a/smoketest/scripts/cli/base_vyostest_shim.py
+++ b/smoketest/scripts/cli/base_vyostest_shim.py
@@ -147,6 +147,18 @@ class VyOSUnitTestSHIM:
break
self.assertTrue(not matched if inverse else matched, msg=search)
+ # Verify ip rule output
+ def verify_rules(self, rules_search, inverse=False, addr_family='inet'):
+ rule_output = cmd(f'ip -family {addr_family} rule show')
+
+ for search in rules_search:
+ matched = False
+ for line in rule_output.split("\n"):
+ if all(item in line for item in search):
+ matched = True
+ break
+ self.assertTrue(not matched if inverse else matched, msg=search)
+
# standard construction; typing suggestion: https://stackoverflow.com/a/70292317
def ignore_warning(warning: Type[Warning]):
import warnings
diff --git a/smoketest/scripts/cli/test_policy_local-route.py b/smoketest/scripts/cli/test_policy_local-route.py
new file mode 100644
index 000000000..8d6ba40dc
--- /dev/null
+++ b/smoketest/scripts/cli/test_policy_local-route.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+interface = 'eth0'
+mark = '100'
+table_id = '101'
+extra_table_id = '102'
+vrf_name = 'LPBRVRF'
+vrf_rt_id = '202'
+
+class TestPolicyLocalRoute(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestPolicyLocalRoute, cls).setUpClass()
+ # Clear out current configuration to allow running this test on a live system
+ cls.cli_delete(cls, ['policy', 'local-route'])
+ cls.cli_delete(cls, ['policy', 'local-route6'])
+
+ cls.cli_set(cls, ['vrf', 'name', vrf_name, 'table', vrf_rt_id])
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.cli_delete(cls, ['vrf', 'name', vrf_name])
+
+ super(TestPolicyLocalRoute, cls).tearDownClass()
+
+ def tearDown(self):
+ self.cli_delete(['policy', 'local-route'])
+ self.cli_delete(['policy', 'local-route6'])
+ self.cli_commit()
+
+ ip_rule_search = [
+ [f'lookup {table_id}']
+ ]
+
+ self.verify_rules(ip_rule_search, inverse=True)
+ self.verify_rules(ip_rule_search, inverse=True, addr_family='inet6')
+
+ def test_local_pbr_matching_criteria(self):
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'inbound-interface', interface])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'protocol', 'udp'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'fwmark', mark])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'destination', 'address', '198.51.100.0/24'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'destination', 'port', '111'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'source', 'address', '198.51.100.1'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'source', 'port', '443'])
+ self.cli_set(['policy', 'local-route', 'rule', '4', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'inbound-interface', interface])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'fwmark', mark])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'destination', 'address', '2001:db8::/64'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'destination', 'port', '123'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'source', 'address', '2001:db8::1'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'source', 'port', '80'])
+ self.cli_set(['policy', 'local-route6', 'rule', '6', 'set', 'table', table_id])
+
+ self.cli_commit()
+
+ rule_lookup = f'lookup {table_id}'
+ rule_fwmark = 'fwmark ' + hex(int(mark))
+ rule_interface = f'iif {interface}'
+
+ ip4_rule_search = [
+ ['from 198.51.100.1', 'to 198.51.100.0/24', rule_fwmark, rule_interface, 'ipproto udp', 'sport 443', 'dport 111', rule_lookup]
+ ]
+
+ self.verify_rules(ip4_rule_search)
+
+ ip6_rule_search = [
+ ['from 2001:db8::1', 'to 2001:db8::/64', rule_fwmark, rule_interface, 'ipproto tcp', 'sport 80', 'dport 123', rule_lookup]
+ ]
+
+ self.verify_rules(ip6_rule_search, addr_family='inet6')
+
+ def test_local_pbr_rule_removal(self):
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.1'])
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'local-route', 'rule', '2', 'destination', 'address', '198.51.100.2'])
+ self.cli_set(['policy', 'local-route', 'rule', '2', 'set', 'table', table_id])
+
+ self.cli_set(['policy', 'local-route', 'rule', '3', 'destination', 'address', '198.51.100.3'])
+ self.cli_set(['policy', 'local-route', 'rule', '3', 'set', 'table', table_id])
+
+ self.cli_commit()
+
+ rule_lookup = f'lookup {table_id}'
+
+ ip_rule_search = [
+ ['to 198.51.100.1', rule_lookup],
+ ['to 198.51.100.2', rule_lookup],
+ ['to 198.51.100.3', rule_lookup],
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+ self.cli_delete(['policy', 'local-route', 'rule', '2'])
+ self.cli_commit()
+
+ ip_rule_missing = [
+ ['to 198.51.100.2', rule_lookup],
+ ]
+
+ self.verify_rules(ip_rule_missing, inverse=True)
+
+ def test_local_pbr_rule_changes(self):
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.0/24'])
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', table_id])
+
+ self.cli_commit()
+
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'table', extra_table_id])
+ self.cli_commit()
+
+ ip_rule_search_extra = [
+ ['to 198.51.100.0/24', f'lookup {extra_table_id}']
+ ]
+
+ self.verify_rules(ip_rule_search_extra)
+
+ ip_rule_search_orig = [
+ ['to 198.51.100.0/24', f'lookup {table_id}']
+ ]
+
+ self.verify_rules(ip_rule_search_orig, inverse=True)
+
+ self.cli_delete(['policy', 'local-route', 'rule', '1', 'set', 'table'])
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'vrf', vrf_name])
+
+ self.cli_commit()
+
+ ip_rule_search_vrf = [
+ ['to 198.51.100.0/24', f'lookup {vrf_name}']
+ ]
+
+ self.verify_rules(ip_rule_search_extra, inverse=True)
+ self.verify_rules(ip_rule_search_vrf)
+
+ def test_local_pbr_target_vrf(self):
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'destination', 'address', '198.51.100.0/24'])
+ self.cli_set(['policy', 'local-route', 'rule', '1', 'set', 'vrf', vrf_name])
+
+ self.cli_commit()
+
+ ip_rule_search = [
+ ['to 198.51.100.0/24', f'lookup {vrf_name}']
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
index 797ab9770..672865eb0 100755
--- a/smoketest/scripts/cli/test_policy_route.py
+++ b/smoketest/scripts/cli/test_policy_route.py
@@ -18,8 +18,6 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-from vyos.utils.process import cmd
-
mark = '100'
conn_mark = '555'
conn_mark_set = '111'
@@ -41,7 +39,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface])
-
+
cls.cli_set(cls, ['vrf', 'name', vrf, 'table', vrf_table_id])
@classmethod
@@ -73,17 +71,6 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
self.verify_rules(ip_rule_search, inverse=True)
- def verify_rules(self, rules_search, inverse=False):
- rule_output = cmd('ip rule show')
-
- for search in rules_search:
- matched = False
- for line in rule_output.split("\n"):
- if all(item in line for item in search):
- matched = True
- break
- self.assertTrue(not matched if inverse else matched, msg=search)
-
def test_pbr_group(self):
self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24'])
self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24'])
diff --git a/smoketest/scripts/cli/test_system_option.py b/smoketest/scripts/cli/test_system_option.py
index ffb1d76ae..ed0280628 100755
--- a/smoketest/scripts/cli/test_system_option.py
+++ b/smoketest/scripts/cli/test_system_option.py
@@ -96,4 +96,4 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase):
if __name__ == '__main__':
- unittest.main(verbosity=2, failfast=True)
+ unittest.main(verbosity=2)
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 215b22b37..233d73ba8 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -27,6 +27,7 @@ from vyos.configdict import node_changed
from vyos.configdiff import Diff
from vyos.configdiff import get_config_diff
from vyos.defaults import directories
+from vyos.pki import encode_certificate
from vyos.pki import is_ca_certificate
from vyos.pki import load_certificate
from vyos.pki import load_public_key
@@ -36,9 +37,11 @@ from vyos.pki import load_private_key
from vyos.pki import load_crl
from vyos.pki import load_dh_parameters
from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.configfs import add_cli_node
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
+from vyos.utils.file import read_file
from vyos.utils.process import call
from vyos.utils.process import cmd
from vyos.utils.process import is_systemd_service_active
@@ -446,9 +449,37 @@ def generate(pki):
# Get foldernames under vyos_certbot_dir which each represent a certbot cert
if os.path.exists(f'{vyos_certbot_dir}/live'):
for cert in certbot_list_on_disk:
+ # ACME certificate is no longer in use by CLI remove it
if cert not in certbot_list:
- # certificate is no longer active on the CLI - remove it
certbot_delete(cert)
+ continue
+ # ACME not enabled for individual certificate - bail out early
+ if 'acme' not in pki['certificate'][cert]:
+ continue
+
+ # Read in ACME certificate chain information
+ tmp = read_file(f'{vyos_certbot_dir}/live/{cert}/chain.pem')
+ tmp = load_certificate(tmp, wrap_tags=False)
+ cert_chain_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1])
+
+ # Check if CA chain certificate is already present on CLI to avoid adding
+ # a duplicate. This only checks for manual added CA certificates and not
+ # auto added ones with the AUTOCHAIN_ prefix
+ autochain_prefix = 'AUTOCHAIN_'
+ ca_cert_present = False
+ if 'ca' in pki:
+ for ca_base64, cli_path in dict_search_recursive(pki['ca'], 'certificate'):
+ # Ignore automatic added CA certificates
+ if any(item.startswith(autochain_prefix) for item in cli_path):
+ continue
+ if cert_chain_base64 == ca_base64:
+ ca_cert_present = True
+
+ if not ca_cert_present:
+ tmp = dict_search_args(pki, 'ca', f'{autochain_prefix}{cert}', 'certificate')
+ if not bool(tmp) or tmp != cert_chain_base64:
+ print(f'Adding/replacing automatically imported CA certificate for "{cert}" ...')
+ add_cli_node(['pki', 'ca', f'{autochain_prefix}{cert}', 'certificate'], value=cert_chain_base64)
return None
diff --git a/src/conf_mode/policy_local-route.py b/src/conf_mode/policy_local-route.py
index 331fd972d..9be2bc227 100755
--- a/src/conf_mode/policy_local-route.py
+++ b/src/conf_mode/policy_local-route.py
@@ -54,6 +54,7 @@ def get_config(config=None):
dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address'])
dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port'])
table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table'])
+ vrf = leaf_node_changed(conf, base_rule + [rule, 'set', 'vrf'])
proto = leaf_node_changed(conf, base_rule + [rule, 'protocol'])
rule_def = {}
if src:
@@ -70,6 +71,8 @@ def get_config(config=None):
rule_def = dict_merge({'destination': {'port': dst_port}}, rule_def)
if table:
rule_def = dict_merge({'table' : table}, rule_def)
+ if vrf:
+ rule_def = dict_merge({'vrf' : vrf}, rule_def)
if proto:
rule_def = dict_merge({'protocol' : proto}, rule_def)
dict = dict_merge({dict_id : {rule : rule_def}}, dict)
@@ -90,6 +93,7 @@ def get_config(config=None):
dst = leaf_node_changed(conf, base_rule + [rule, 'destination', 'address'])
dst_port = leaf_node_changed(conf, base_rule + [rule, 'destination', 'port'])
table = leaf_node_changed(conf, base_rule + [rule, 'set', 'table'])
+ vrf = leaf_node_changed(conf, base_rule + [rule, 'set', 'vrf'])
proto = leaf_node_changed(conf, base_rule + [rule, 'protocol'])
# keep track of changes in configuration
# otherwise we might remove an existing node although nothing else has changed
@@ -179,6 +183,15 @@ def get_config(config=None):
if len(table) > 0:
rule_def = dict_merge({'table' : table}, rule_def)
+ # vrf
+ if vrf is None:
+ if 'set' in rule_config and 'vrf' in rule_config['set']:
+ rule_def = dict_merge({'vrf': [rule_config['set']['vrf']]}, rule_def)
+ else:
+ changed = True
+ if len(vrf) > 0:
+ rule_def = dict_merge({'vrf' : vrf}, rule_def)
+
# protocol
if proto is None:
if 'protocol' in rule_config:
@@ -218,8 +231,15 @@ def verify(pbr):
):
raise ConfigError('Source or destination address or fwmark or inbound-interface or protocol is required!')
- if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']:
- raise ConfigError('Table set is required!')
+ if 'set' not in pbr_route['rule'][rule]:
+ raise ConfigError('Either set table or set vrf is required!')
+
+ set_tgts = pbr_route['rule'][rule]['set']
+ if 'table' not in set_tgts and 'vrf' not in set_tgts:
+ raise ConfigError('Either set table or set vrf is required!')
+
+ if 'table' in set_tgts and 'vrf' in set_tgts:
+ raise ConfigError('set table and set vrf cannot both be set!')
if 'inbound_interface' in pbr_route['rule'][rule]:
interface = pbr_route['rule'][rule]['inbound_interface']
@@ -250,11 +270,14 @@ def apply(pbr):
fwmark = rule_config.get('fwmark', [''])
inbound_interface = rule_config.get('inbound_interface', [''])
protocol = rule_config.get('protocol', [''])
- table = rule_config.get('table', [''])
+ # VRF 'default' is actually table 'main' for RIB rules
+ vrf = [ 'main' if x == 'default' else x for x in rule_config.get('vrf', ['']) ]
+ # See generate section below for table/vrf overlap explanation
+ table_or_vrf = rule_config.get('table', vrf)
- for src, dst, src_port, dst_port, fwmk, iif, proto, table in product(
+ for src, dst, src_port, dst_port, fwmk, iif, proto, table_or_vrf in product(
source, destination, source_port, destination_port,
- fwmark, inbound_interface, protocol, table):
+ fwmark, inbound_interface, protocol, table_or_vrf):
f_src = '' if src == '' else f' from {src} '
f_src_port = '' if src_port == '' else f' sport {src_port} '
f_dst = '' if dst == '' else f' to {dst} '
@@ -262,7 +285,7 @@ def apply(pbr):
f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} '
f_iif = '' if iif == '' else f' iif {iif} '
f_proto = '' if proto == '' else f' ipproto {proto} '
- f_table = '' if table == '' else f' lookup {table} '
+ f_table = '' if table_or_vrf == '' else f' lookup {table_or_vrf} '
call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif}{f_table}')
@@ -276,7 +299,13 @@ def apply(pbr):
if 'rule' in pbr_route:
for rule, rule_config in pbr_route['rule'].items():
- table = rule_config['set'].get('table', '')
+ # VRFs get configred as route table alias names for iproute2 and only
+ # one 'set' can get past validation. Either can be fed to lookup.
+ vrf = rule_config['set'].get('vrf', '')
+ if vrf == 'default':
+ table_or_vrf = 'main'
+ else:
+ table_or_vrf = rule_config['set'].get('table', vrf)
source = rule_config.get('source', {}).get('address', ['all'])
source_port = rule_config.get('source', {}).get('port', '')
destination = rule_config.get('destination', {}).get('address', ['all'])
@@ -295,7 +324,7 @@ def apply(pbr):
f_iif = f' iif {inbound_interface} ' if inbound_interface else ''
f_proto = f' ipproto {protocol} ' if protocol else ''
- call(f'ip{v6} rule add prio {rule}{f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif} lookup {table}')
+ call(f'ip{v6} rule add prio {rule}{f_src}{f_dst}{f_proto}{f_src_port}{f_dst_port}{f_fwmk}{f_iif} lookup {table_or_vrf}')
return None
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index a2373218a..430cc69d4 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -88,7 +88,7 @@ def verify(static):
if {'blackhole', 'reject'} <= set(prefix_options):
raise ConfigError(f'Can not use both blackhole and reject for '\
- 'prefix "{prefix}"!')
+ f'prefix "{prefix}"!')
return None
diff --git a/src/op_mode/mtr.py b/src/op_mode/mtr.py
index de139f2fa..baf9672a1 100644
--- a/src/op_mode/mtr.py
+++ b/src/op_mode/mtr.py
@@ -178,6 +178,7 @@ mtr = {
6: '/bin/mtr -6',
}
+
class List(list):
def first(self):
return self.pop(0) if self else ''
@@ -218,12 +219,15 @@ def complete(prefix):
def convert(command, args):
+ to_json = False
while args:
shortname = args.first()
longnames = complete(shortname)
if len(longnames) != 1:
expension_failure(shortname, longnames)
longname = longnames[0]
+ if longname == 'json':
+ to_json = True
if options[longname]['type'] == 'noarg':
command = options[longname]['mtr'].format(
command=command, value='')
@@ -232,7 +236,7 @@ def convert(command, args):
else:
command = options[longname]['mtr'].format(
command=command, value=args.first())
- return command
+ return command, to_json
if __name__ == '__main__':
@@ -242,7 +246,6 @@ if __name__ == '__main__':
if not host:
sys.exit("mtr: Missing host")
-
if host == '--get-options' or host == '--get-options-nested':
if host == '--get-options-nested':
args.first() # pop monitor
@@ -302,5 +305,8 @@ if __name__ == '__main__':
except ValueError:
sys.exit(f'mtr: Unknown host: {host}')
- command = convert(mtr[version], args)
- call(f'{command} --curses --displaymode 0 {host}')
+ command, to_json = convert(mtr[version], args)
+ if to_json:
+ call(f'{command} {host}')
+ else:
+ call(f'{command} --curses --displaymode 0 {host}')
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index ab613e5c4..5652a5d74 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -26,13 +26,22 @@ from cryptography.x509.oid import ExtendedKeyUsageOID
from vyos.config import Config
from vyos.config import config_dict_mangle_acme
-from vyos.pki import encode_certificate, encode_public_key, encode_private_key, encode_dh_parameters
+from vyos.pki import encode_certificate
+from vyos.pki import encode_public_key
+from vyos.pki import encode_private_key
+from vyos.pki import encode_dh_parameters
from vyos.pki import get_certificate_fingerprint
-from vyos.pki import create_certificate, create_certificate_request, create_certificate_revocation_list
+from vyos.pki import create_certificate
+from vyos.pki import create_certificate_request
+from vyos.pki import create_certificate_revocation_list
from vyos.pki import create_private_key
from vyos.pki import create_dh_parameters
-from vyos.pki import load_certificate, load_certificate_request, load_private_key
-from vyos.pki import load_crl, load_dh_parameters, load_public_key
+from vyos.pki import load_certificate
+from vyos.pki import load_certificate_request
+from vyos.pki import load_private_key
+from vyos.pki import load_crl
+from vyos.pki import load_dh_parameters
+from vyos.pki import load_public_key
from vyos.pki import verify_certificate
from vyos.utils.io import ask_input
from vyos.utils.io import ask_yes_no
diff --git a/src/utils/vyos-commands-to-config b/src/utils/vyos-commands-to-config
new file mode 100755
index 000000000..927d9bd70
--- /dev/null
+++ b/src/utils/vyos-commands-to-config
@@ -0,0 +1,53 @@
+#! /usr/bin/python3
+#
+# Copyright (C) 2024 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 sys
+import json
+
+from vyos.configtree import ConfigTree
+from vyos.utils.config import parse_commands
+from vyos.utils.config import set_tags
+
+def commands_to_config(cmds):
+ ct = ConfigTree('')
+ cmds = parse_commands(cmds)
+
+ for c in cmds:
+ if c["op"] == "set":
+ if c["is_leaf"]:
+ replace = False if c["is_multi"] else True
+ ct.set(c["path"], value=c["value"], replace=replace)
+ set_tags(ct, c["path"])
+ else:
+ ct.create_node(c["path"])
+ set_tags(ct, c["path"])
+ else:
+ raise ValueError(
+ f"\"{c['op']}\" is not a supported config operation")
+
+ return ct
+
+
+if __name__ == '__main__':
+ try:
+ cmds = sys.stdin.read()
+ ct = commands_to_config(cmds)
+ out = ConfigTree(ct.to_string())
+ print(str(out))
+ except Exception as e:
+ print(e)
+ sys.exit(1)