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