diff options
25 files changed, 916 insertions, 59 deletions
diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml index 467ff062e..a74c35adf 100644 --- a/.github/workflows/package-smoketest.yml +++ b/.github/workflows/package-smoketest.yml @@ -15,6 +15,9 @@ permissions: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed for PR comments + BUILD_BY: autobuild@vyos.net + DEBIAN_MIRROR: http://deb.debian.org/debian/ + VYOS_MIRROR: https://rolling-packages.vyos.net/current/ jobs: build_iso: @@ -23,9 +26,6 @@ jobs: container: image: vyos/vyos-build:current options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged - env: - BUILD_BY: autobuild@vyos.net - DEBIAN_MIRROR: http://deb.debian.org/debian/ outputs: build_version: ${{ steps.version.outputs.build_version }} steps: @@ -52,9 +52,11 @@ jobs: sudo --preserve-env ./build-vyos-image \ --architecture amd64 \ --build-by $BUILD_BY \ + --build-type release \ + --custom-package vyos-1x-smoketest \ --debian-mirror $DEBIAN_MIRROR \ --version ${{ steps.version.outputs.build_version }} \ - --build-type release \ + --vyos-mirror $VYOS_MIRROR \ generic - uses: actions/upload-artifact@v4 with: @@ -154,11 +156,43 @@ jobs: echo "exit_code=fail" >> $GITHUB_OUTPUT fi + test_encrypted_config_tpm: + needs: build_iso + runs-on: ubuntu-24.04 + timeout-minutes: 30 + 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 TPM encryption tests + id: test + shell: bash + run: | + set -e + sudo make testtpm + if [[ $? == 0 ]]; then + echo "exit_code=success" >> $GITHUB_OUTPUT + else + echo "exit_code=fail" >> $GITHUB_OUTPUT + fi + result: needs: - test_smoketest_cli - test_config_load - test_raid1_install + - test_encrypted_config_tpm runs-on: ubuntu-24.04 timeout-minutes: 5 if: always() @@ -177,6 +211,7 @@ jobs: * CLI Smoketests ${{ needs.test_smoketest_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' }} message-id: "SMOKETEST_RESULTS" allow-repeats: false diff --git a/.github/workflows/trigger-rebuild-repo-package.yml b/.github/workflows/trigger-rebuild-repo-package.yml index d0936b572..37ec83274 100644 --- a/.github/workflows/trigger-rebuild-repo-package.yml +++ b/.github/workflows/trigger-rebuild-repo-package.yml @@ -1,7 +1,7 @@ name: Trigger to build a deb package from repo on: - pull_request: + pull_request_target: types: - closed branches: @@ -23,11 +23,10 @@ jobs: needs: get_repo_name uses: vyos/.github/.github/workflows/trigger-rebuild-repo-package.yml@current with: - branch: ${{ github.ref_name }} - package_name: ${{ needs.get_repo_name.outputs.PACKAGE_NAME }} - REF: main # optinal because the default value is main + branch: ${{ github.ref_name }} + package_name: ${{ needs.get_repo_name.outputs.PACKAGE_NAME }} secrets: - REMOTE_OWNER: ${{ secrets.REMOTE_OWNER }} - REMOTE_REUSE_REPO: ${{ secrets.REMOTE_REUSE_REPO }} - GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} - PAT: ${{ secrets.PAT }} + REMOTE_OWNER: ${{ secrets.REMOTE_OWNER }} + REMOTE_REUSE_REPO: ${{ secrets.REMOTE_REUSE_REPO }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + PAT: ${{ secrets.PAT }} diff --git a/data/templates/frr/daemons.frr.tmpl b/data/templates/frr/daemons.frr.tmpl index 339b4e52f..3506528d2 100644 --- a/data/templates/frr/daemons.frr.tmpl +++ b/data/templates/frr/daemons.frr.tmpl @@ -36,7 +36,7 @@ babeld=yes sharpd=no pbrd=no bfdd=yes -fabricd=no +fabricd=yes vrrpd=no pathd=no diff --git a/data/templates/frr/fabricd.frr.j2 b/data/templates/frr/fabricd.frr.j2 new file mode 100644 index 000000000..8f2ae6466 --- /dev/null +++ b/data/templates/frr/fabricd.frr.j2 @@ -0,0 +1,72 @@ +! +{% for name, router_config in domain.items() %} +{% if router_config.interface is vyos_defined %} +{% for iface, iface_config in router_config.interface.items() %} +interface {{ iface }} +{% if iface_config.address_family.ipv4 is vyos_defined %} + ip router openfabric {{ name }} +{% endif %} +{% if iface_config.address_family.ipv6 is vyos_defined %} + ipv6 router openfabric {{ name }} +{% endif %} +{% if iface_config.csnp_interval is vyos_defined %} + openfabric csnp-interval {{ iface_config.csnp_interval }} +{% endif %} +{% if iface_config.hello_interval is vyos_defined %} + openfabric hello-interval {{ iface_config.hello_interval }} +{% endif %} +{% if iface_config.hello_multiplier is vyos_defined %} + openfabric hello-multiplier {{ iface_config.hello_multiplier }} +{% endif %} +{% if iface_config.metric is vyos_defined %} + openfabric metric {{ iface_config.metric }} +{% endif %} +{% if iface_config.passive is vyos_defined or iface == 'lo' %} + openfabric passive +{% endif %} +{% if iface_config.password.md5 is vyos_defined %} + openfabric password md5 {{ iface_config.password.md5 }} +{% elif iface_config.password.plaintext_password is vyos_defined %} + openfabric password clear {{ iface_config.password.plaintext_password }} +{% endif %} +{% if iface_config.psnp_interval is vyos_defined %} + openfabric psnp-interval {{ iface_config.psnp_interval }} +{% endif %} +exit +! +{% endfor %} +{% endif %} +router openfabric {{ name }} + net {{ net }} +{% if router_config.domain_password.md5 is vyos_defined %} + domain-password md5 {{ router_config.domain_password.plaintext_password }} +{% elif router_config.domain_password.plaintext_password is vyos_defined %} + domain-password clear {{ router_config.domain_password.plaintext_password }} +{% endif %} +{% if router_config.log_adjacency_changes is vyos_defined %} + log-adjacency-changes +{% endif %} +{% if router_config.set_overload_bit is vyos_defined %} + set-overload-bit +{% endif %} +{% if router_config.purge_originator is vyos_defined %} + purge-originator +{% endif %} +{% if router_config.fabric_tier is vyos_defined %} + fabric-tier {{ router_config.fabric_tier }} +{% endif %} +{% if router_config.lsp_gen_interval is vyos_defined %} + lsp-gen-interval {{ router_config.lsp_gen_interval }} +{% endif %} +{% if router_config.lsp_refresh_interval is vyos_defined %} + lsp-refresh-interval {{ router_config.lsp_refresh_interval }} +{% endif %} +{% if router_config.max_lsp_lifetime is vyos_defined %} + max-lsp-lifetime {{ router_config.max_lsp_lifetime }} +{% endif %} +{% if router_config.spf_interval is vyos_defined %} + spf-interval {{ router_config.spf_interval }} +{% endif %} +exit +! +{% endfor %} diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i index 0e79ca5f2..35ce80be9 100644 --- a/interface-definitions/include/isis/protocol-common-config.xml.i +++ b/interface-definitions/include/isis/protocol-common-config.xml.i @@ -86,12 +86,7 @@ </constraint> </properties> </leafNode> -<leafNode name="log-adjacency-changes"> - <properties> - <help>Log adjacency state changes</help> - <valueless/> - </properties> -</leafNode> +#include <include/log-adjacency-changes.xml.i> <leafNode name="lsp-gen-interval"> <properties> <help>Minimum interval between regenerating same LSP</help> @@ -208,18 +203,7 @@ #include <include/isis/lfa-protocol.xml.i> </children> </node> -<leafNode name="net"> - <properties> - <help>A Network Entity Title for this process (ISO only)</help> - <valueHelp> - <format>XX.XXXX. ... .XXX.XX</format> - <description>Network entity title (NET)</description> - </valueHelp> - <constraint> - <regex>[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}</regex> - </constraint> - </properties> -</leafNode> +#include <include/net.xml.i> <leafNode name="purge-originator"> <properties> <help>Use the RFC 6232 purge-originator</help> diff --git a/interface-definitions/include/log-adjacency-changes.xml.i b/interface-definitions/include/log-adjacency-changes.xml.i new file mode 100644 index 000000000..a0628b8e2 --- /dev/null +++ b/interface-definitions/include/log-adjacency-changes.xml.i @@ -0,0 +1,8 @@ +<!-- include start from log-adjacency-changes.xml.i --> +<leafNode name="log-adjacency-changes"> + <properties> + <help>Log changes in adjacency state</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/net.xml.i b/interface-definitions/include/net.xml.i new file mode 100644 index 000000000..10b54ee49 --- /dev/null +++ b/interface-definitions/include/net.xml.i @@ -0,0 +1,14 @@ +<!-- include start from net.xml.i --> +<leafNode name="net"> + <properties> + <help>A Network Entity Title for the process (ISO only)</help> + <valueHelp> + <format>XX.XXXX. ... .XXX.XX</format> + <description>Network entity title (NET)</description> + </valueHelp> + <constraint> + <regex>[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}</regex> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/openfabric/password.xml.i b/interface-definitions/include/openfabric/password.xml.i new file mode 100644 index 000000000..fa34a4dab --- /dev/null +++ b/interface-definitions/include/openfabric/password.xml.i @@ -0,0 +1,20 @@ +<!-- include start from openfabric/password.xml.i --> +<leafNode name="plaintext-password"> + <properties> + <help>Use plain text password</help> + <valueHelp> + <format>txt</format> + <description>Authentication password</description> + </valueHelp> + </properties> +</leafNode> +<leafNode name="md5"> + <properties> + <help>Use MD5 hash authentication</help> + <valueHelp> + <format>txt</format> + <description>Authentication password</description> + </valueHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/protocols_openfabric.xml.in b/interface-definitions/protocols_openfabric.xml.in new file mode 100644 index 000000000..81200360e --- /dev/null +++ b/interface-definitions/protocols_openfabric.xml.in @@ -0,0 +1,218 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="openfabric" owner="${vyos_conf_scripts_dir}/protocols_openfabric.py"> + <properties> + <help>OpenFabric protocol</help> + <priority>680</priority> + </properties> + <children> + #include <include/net.xml.i> + <tagNode name="domain"> + <properties> + <help>OpenFabric process name</help> + <valueHelp> + <format>txt</format> + <description>Domain name</description> + </valueHelp> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Interface params</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> + </properties> + <children> + <node name="address-family"> + <properties> + <help>Openfabric address family</help> + </properties> + <children> + <leafNode name="ipv4"> + <properties> + <help>IPv4 OpenFabric</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ipv6"> + <properties> + <help>IPv6 OpenFabric</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="csnp-interval"> + <properties> + <help>Complete Sequence Number Packets (CSNP) interval</help> + <valueHelp> + <format>u32:1-600</format> + <description>CSNP interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-600"/> + </constraint> + </properties> + </leafNode> + <leafNode name="hello-interval"> + <properties> + <help>Hello interval</help> + <valueHelp> + <format>u32:1-600</format> + <description>Hello interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-600"/> + </constraint> + </properties> + </leafNode> + <leafNode name="hello-multiplier"> + <properties> + <help>Multiplier for Hello holding time</help> + <valueHelp> + <format>u32:2-100</format> + <description>Multiplier for Hello holding time</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 2-100"/> + </constraint> + </properties> + </leafNode> + <leafNode name="metric"> + <properties> + <help>Interface metric value</help> + <valueHelp> + <format>u32:0-16777215</format> + <description>Interface metric value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777215"/> + </constraint> + </properties> + </leafNode> + <leafNode name="passive"> + <properties> + <help>Do not initiate adjacencies to the interface</help> + <valueless/> + </properties> + </leafNode> + <node name="password"> + <properties> + <help>Authentication password for the interface</help> + </properties> + <children> + #include <include/openfabric/password.xml.i> + </children> + </node> + <leafNode name="psnp-interval"> + <properties> + <help>Partial Sequence Number Packets (PSNP) interval</help> + <valueHelp> + <format>u32:0-120</format> + <description>PSNP interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-120"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <node name="domain-password"> + <properties> + <help>Authentication password for a routing domain</help> + </properties> + <children> + #include <include/openfabric/password.xml.i> + </children> + </node> + #include <include/log-adjacency-changes.xml.i> + <leafNode name="set-overload-bit"> + <properties> + <help>Overload bit to avoid any transit traffic</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="purge-originator"> + <properties> + <help>RFC 6232 purge originator identification</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="fabric-tier"> + <properties> + <help>Static tier number to advertise as location in the fabric</help> + <valueHelp> + <format>u32:0-14</format> + <description>Static tier number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-14"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lsp-gen-interval"> + <properties> + <help>Minimum interval between regenerating same link-state packet (LSP)</help> + <valueHelp> + <format>u32:1-120</format> + <description>Minimum interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-120"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lsp-refresh-interval"> + <properties> + <help>Link-state packet (LSP) refresh interval</help> + <valueHelp> + <format>u32:1-65235</format> + <description>LSP refresh interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65235"/> + </constraint> + </properties> + </leafNode> + <leafNode name="max-lsp-lifetime"> + <properties> + <help>Maximum link-state packet lifetime</help> + <valueHelp> + <format>u32:360-65535</format> + <description>Maximum LSP lifetime in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 360-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="spf-interval"> + <properties> + <help>Minimum interval between SPF calculations</help> + <valueHelp> + <format>u32:1-120</format> + <description>Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-120"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system_option.xml.in b/interface-definitions/system_option.xml.in index e78a53552..dc9958ff5 100644 --- a/interface-definitions/system_option.xml.in +++ b/interface-definitions/system_option.xml.in @@ -49,6 +49,26 @@ <valueless/> </properties> </leafNode> + <leafNode name="amd-pstate-driver"> + <properties> + <help>Enables and configures pstate driver for AMD Ryzen and Epyc CPUs</help> + <completionHelp> + <list>active passive guided</list> + </completionHelp> + <valueHelp> + <format>active</format> + <description>The firmware controls performance states and the system governor has no effect</description> + </valueHelp> + <valueHelp> + <format>passive</format> + <description>Allow the system governor to manage performance states</description> + </valueHelp> + <valueHelp> + <format>guided</format> + <description>The firmware controls performance states guided by the system governor</description> + </valueHelp> + </properties> + </leafNode> <node name="debug"> <properties> <help>Dynamic debugging for kernel module</help> diff --git a/op-mode-definitions/execute-ssh.xml.in b/op-mode-definitions/execute-ssh.xml.in new file mode 100644 index 000000000..7fa656f5e --- /dev/null +++ b/op-mode-definitions/execute-ssh.xml.in @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="execute"> + <children> + <node name="ssh"> + <properties> + <help>SSH to a node</help> + </properties> + <children> + <tagNode name="host"> + <properties> + <help>Hostname or IP address</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/ssh $4</command> + <children> + <tagNode name="user"> + <properties> + <help>Remote server username</help> + <completionHelp> + <list><username></list> + </completionHelp> + </properties> + <command>/usr/bin/ssh $6@$4</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/include/show-route-openfabric.xml.i b/op-mode-definitions/include/show-route-openfabric.xml.i new file mode 100644 index 000000000..ae1ef380e --- /dev/null +++ b/op-mode-definitions/include/show-route-openfabric.xml.i @@ -0,0 +1,8 @@ +<!-- included start from show-route-openfabric.xml.i --> +<leafNode name="openfabric"> + <properties> + <help>OpenFabric routes</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index a2d5d924a..6a2b7e53b 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -237,6 +237,12 @@ </properties> <command>journalctl --follow --no-hostname --boot /usr/lib/frr/isisd</command> </leafNode> + <leafNode name="openfabric"> + <properties> + <help>Monitor log for OpenFabric</help> + </properties> + <command>journalctl --follow --no-hostname --boot /usr/lib/frr/fabricd</command> + </leafNode> <leafNode name="nhrp"> <properties> <help>Monitor log for NHRP</help> diff --git a/op-mode-definitions/restart-frr.xml.in b/op-mode-definitions/restart-frr.xml.in index 2c9d4b1cc..4772e8dd2 100644 --- a/op-mode-definitions/restart-frr.xml.in +++ b/op-mode-definitions/restart-frr.xml.in @@ -56,6 +56,12 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon isisd</command> </leafNode> + <leafNode name="openfabric"> + <properties> + <help>Restart OpenFabric routing daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon fabricd</command> + </leafNode> <leafNode name="pim6"> <properties> <help>Restart IPv6 Protocol Independent Multicast (PIM) daemon</help> diff --git a/op-mode-definitions/show-ip-route.xml.in b/op-mode-definitions/show-ip-route.xml.in index c878bf712..37279d3d2 100644 --- a/op-mode-definitions/show-ip-route.xml.in +++ b/op-mode-definitions/show-ip-route.xml.in @@ -46,6 +46,7 @@ <command>ip -s route list $5</command> </tagNode> #include <include/show-route-isis.xml.i> + #include <include/show-route-openfabric.xml.i> #include <include/show-route-kernel.xml.i> #include <include/show-route-ospf.xml.i> #include <include/show-route-rip.xml.i> diff --git a/op-mode-definitions/show-ipv6-route.xml.in b/op-mode-definitions/show-ipv6-route.xml.in index d73fb46b4..f68a94971 100644 --- a/op-mode-definitions/show-ipv6-route.xml.in +++ b/op-mode-definitions/show-ipv6-route.xml.in @@ -46,6 +46,7 @@ <command>ip -s -f inet6 route list $5</command> </tagNode> #include <include/show-route-isis.xml.i> + #include <include/show-route-openfabric.xml.i> #include <include/show-route-kernel.xml.i> #include <include/show-route-ospfv3.xml.i> #include <include/show-route-ripng.xml.i> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 7ae3b890b..f0fad63d2 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -642,6 +642,12 @@ </properties> <command>journalctl --boot /usr/lib/frr/isisd</command> </leafNode> + <leafNode name="openfabric"> + <properties> + <help>Show log for OpenFabric</help> + </properties> + <command>journalctl --boot /usr/lib/frr/fabricd</command> + </leafNode> <leafNode name="nhrp"> <properties> <help>Show log for NHRP</help> diff --git a/op-mode-definitions/show-openfabric.xml.in b/op-mode-definitions/show-openfabric.xml.in new file mode 100644 index 000000000..2f489866e --- /dev/null +++ b/op-mode-definitions/show-openfabric.xml.in @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="openfabric"> + <properties> + <help>Show OpenFabric routing protocol</help> + </properties> + <children> + <node name="database"> + <properties> + <help>Show OpenFabric link state database</help> + </properties> + <children> + #include <include/vtysh-generic-detail.xml.i> + </children> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + <node name="interface"> + <properties> + <help>Show OpenFabric interfaces</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + <children> + #include <include/vtysh-generic-detail.xml.i> + </children> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + #include <include/vtysh-generic-interface-tagNode.xml.i> + <node name="neighbor"> + <properties> + <help>Show OpenFabric neighbor adjacencies</help> + </properties> + <children> + #include <include/vtysh-generic-detail.xml.i> + </children> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + <leafNode name="summary"> + <properties> + <help>Show OpenFabric information summary</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/frr.py b/python/vyos/frr.py index e7743e9d5..6fb81803f 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -87,7 +87,7 @@ LOG.addHandler(ch) LOG.addHandler(ch2) _frr_daemons = ['zebra', 'staticd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', - 'isisd', 'pimd', 'pim6d', 'ldpd', 'eigrpd', 'babeld', 'bfdd'] + 'isisd', 'pimd', 'pim6d', 'ldpd', 'eigrpd', 'babeld', 'bfdd', 'fabricd'] path_vtysh = '/usr/bin/vtysh' path_frr_reload = '/usr/lib/frr/frr-reload.py' diff --git a/smoketest/scripts/cli/test_protocols_openfabric.py b/smoketest/scripts/cli/test_protocols_openfabric.py new file mode 100644 index 000000000..e37aed456 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_openfabric.py @@ -0,0 +1,186 @@ +#!/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 +from vyos.configsession import ConfigSessionError +from vyos.utils.process import process_named_running + +PROCESS_NAME = 'fabricd' +base_path = ['protocols', 'openfabric'] + +domain = 'VyOS' +net = '49.0001.1111.1111.1111.00' +dummy_if = 'dum1234' +address_families = ['ipv4', 'ipv6'] + +path = base_path + ['domain', domain] + +class TestProtocolsOpenFabric(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsOpenFabric, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + + def openfabric_base_config(self): + self.cli_set(['interfaces', 'dummy', dummy_if]) + self.cli_set(base_path + ['net', net]) + for family in address_families: + self.cli_set(path + ['interface', dummy_if, 'address-family', family]) + + def test_openfabric_01_router_params(self): + fabric_tier = '5' + lsp_gen_interval = '20' + + self.cli_set(base_path) + + # verify() - net id and domain name are mandatory + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.openfabric_base_config() + + self.cli_set(path + ['log-adjacency-changes']) + self.cli_set(path + ['set-overload-bit']) + self.cli_set(path + ['fabric-tier', fabric_tier]) + self.cli_set(path + ['lsp-gen-interval', lsp_gen_interval]) + + # Commit all changes + self.cli_commit() + + # Verify all changes + tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' log-adjacency-changes', tmp) + self.assertIn(f' set-overload-bit', tmp) + self.assertIn(f' fabric-tier {fabric_tier}', tmp) + self.assertIn(f' lsp-gen-interval {lsp_gen_interval}', tmp) + + tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd') + self.assertIn(f' ip router openfabric {domain}', tmp) + self.assertIn(f' ipv6 router openfabric {domain}', tmp) + + def test_openfabric_02_loopback_interface(self): + interface = 'lo' + hello_interval = '100' + metric = '24478' + + self.openfabric_base_config() + self.cli_set(path + ['interface', interface, 'address-family', 'ipv4']) + + self.cli_set(path + ['interface', interface, 'hello-interval', hello_interval]) + self.cli_set(path + ['interface', interface, 'metric', metric]) + + # Commit all changes + self.cli_commit() + + # Verify FRR openfabric configuration + tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd') + self.assertIn(f'router openfabric {domain}', tmp) + self.assertIn(f' net {net}', tmp) + + # Verify interface configuration + tmp = self.getFRRconfig(f'interface {interface}', daemon='fabricd') + self.assertIn(f' ip router openfabric {domain}', tmp) + # for lo interface 'openfabric passive' is implied + self.assertIn(f' openfabric passive', tmp) + self.assertIn(f' openfabric metric {metric}', tmp) + + def test_openfabric_03_password(self): + password = 'foo' + + self.openfabric_base_config() + + self.cli_set(path + ['interface', dummy_if, 'password', 'plaintext-password', f'{password}-{dummy_if}']) + self.cli_set(path + ['interface', dummy_if, 'password', 'md5', f'{password}-{dummy_if}']) + + # verify() - can not use both md5 and plaintext-password for password for the interface + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(path + ['interface', dummy_if, 'password', 'md5']) + + self.cli_set(path + ['domain-password', 'plaintext-password', password]) + self.cli_set(path + ['domain-password', 'md5', password]) + + # verify() - can not use both md5 and plaintext-password for domain-password + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(path + ['domain-password', 'md5']) + + # Commit all changes + self.cli_commit() + + # Verify all changes + tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd') + self.assertIn(f' net {net}', tmp) + self.assertIn(f' domain-password clear {password}', tmp) + + tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd') + self.assertIn(f' openfabric password clear {password}-{dummy_if}', tmp) + + def test_openfabric_multiple_domains(self): + domain_2 = 'VyOS_2' + interface = 'dum5678' + new_path = base_path + ['domain', domain_2] + + self.openfabric_base_config() + + # set same interface for 2 OpenFabric domains + self.cli_set(['interfaces', 'dummy', interface]) + self.cli_set(new_path + ['interface', interface, 'address-family', 'ipv4']) + self.cli_set(path + ['interface', interface, 'address-family', 'ipv4']) + + # verify() - same interface can be used only for one OpenFabric instance + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(path + ['interface', interface]) + + # Commit all changes + self.cli_commit() + + # Verify FRR openfabric configuration + tmp = self.getFRRconfig(f'router openfabric {domain}', daemon='fabricd') + self.assertIn(f'router openfabric {domain}', tmp) + self.assertIn(f' net {net}', tmp) + + tmp = self.getFRRconfig(f'router openfabric {domain_2}', daemon='fabricd') + self.assertIn(f'router openfabric {domain_2}', tmp) + self.assertIn(f' net {net}', tmp) + + # Verify interface configuration + tmp = self.getFRRconfig(f'interface {dummy_if}', daemon='fabricd') + self.assertIn(f' ip router openfabric {domain}', tmp) + self.assertIn(f' ipv6 router openfabric {domain}', tmp) + + tmp = self.getFRRconfig(f'interface {interface}', daemon='fabricd') + self.assertIn(f' ip router openfabric {domain_2}', tmp) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index 4df893ebf..a5963e72c 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -167,10 +167,10 @@ def verify(policy): continue for rule, rule_config in route_map_config['rule'].items(): - # Action 'deny' cannot be used with "continue" - # FRR does not validate it T4827 - if rule_config['action'] == 'deny' and 'continue' in rule_config: - raise ConfigError(f'rule {rule} "continue" cannot be used with action deny!') + # Action 'deny' cannot be used with "continue" or "on-match" + # FRR does not validate it T4827, T6676 + if rule_config['action'] == 'deny' and ('continue' in rule_config or 'on_match' in rule_config): + raise ConfigError(f'rule {rule} "continue" or "on-match" cannot be used with action deny!') # Specified community-list must exist tmp = dict_search('match.community.community_list', diff --git a/src/conf_mode/protocols_openfabric.py b/src/conf_mode/protocols_openfabric.py new file mode 100644 index 000000000..8e8c50c06 --- /dev/null +++ b/src/conf_mode/protocols_openfabric.py @@ -0,0 +1,145 @@ +#!/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/>. + +from sys import exit + +from vyos.base import Warning +from vyos.config import Config +from vyos.configdict import node_changed +from vyos.configverify import verify_interface_exists +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr +from vyos import airbag + +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base_path = ['protocols', 'openfabric'] + + openfabric = conf.get_config_dict(base_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + # Remove per domain MPLS configuration - get a list of all changed Openfabric domains + # (removed and added) so that they will be properly rendered for the FRR config. + openfabric['domains_all'] = list(conf.list_nodes(' '.join(base_path) + f' domain') + + node_changed(conf, base_path + ['domain'])) + + # Get a list of all interfaces + openfabric['interfaces_all'] = [] + for domain in openfabric['domains_all']: + interfaces_modified = list(node_changed(conf, base_path + ['domain', domain, 'interface']) + + conf.list_nodes(' '.join(base_path) + f' domain {domain} interface')) + openfabric['interfaces_all'].extend(interfaces_modified) + + if not conf.exists(base_path): + openfabric.update({'deleted': ''}) + + return openfabric + +def verify(openfabric): + # bail out early - looks like removal from running config + if not openfabric or 'deleted' in openfabric: + return None + + if 'net' not in openfabric: + raise ConfigError('Network entity is mandatory!') + + # last byte in OpenFabric area address must be 0 + tmp = openfabric['net'].split('.') + if int(tmp[-1]) != 0: + raise ConfigError('Last byte of OpenFabric network entity title must always be 0!') + + if 'domain' not in openfabric: + raise ConfigError('OpenFabric domain name is mandatory!') + + interfaces_used = [] + + for domain, domain_config in openfabric['domain'].items(): + # If interface not set + if 'interface' not in domain_config: + raise ConfigError(f'Interface used for routing updates in OpenFabric "{domain}" is mandatory!') + + for iface, iface_config in domain_config['interface'].items(): + verify_interface_exists(openfabric, iface) + + # interface can be activated only on one OpenFabric instance + if iface in interfaces_used: + raise ConfigError(f'Interface {iface} is already used in different OpenFabric instance!') + + if 'address_family' not in iface_config or len(iface_config['address_family']) < 1: + raise ConfigError(f'Need to specify address family for the interface "{iface}"!') + + # If md5 and plaintext-password set at the same time + if 'password' in iface_config: + if {'md5', 'plaintext_password'} <= set(iface_config['password']): + raise ConfigError(f'Can use either md5 or plaintext-password for password for the interface!') + + if iface == 'lo' and 'passive' not in iface_config: + Warning('For loopback interface passive mode is implied!') + + interfaces_used.append(iface) + + # If md5 and plaintext-password set at the same time + password = 'domain_password' + if password in domain_config: + if {'md5', 'plaintext_password'} <= set(domain_config[password]): + raise ConfigError(f'Can use either md5 or plaintext-password for domain-password!') + + return None + +def generate(openfabric): + if not openfabric or 'deleted' in openfabric: + return None + + openfabric['frr_fabricd_config'] = render_to_string('frr/fabricd.frr.j2', openfabric) + return None + +def apply(openfabric): + openfabric_daemon = 'fabricd' + + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + frr_cfg.load_configuration(openfabric_daemon) + for domain in openfabric['domains_all']: + frr_cfg.modify_section(f'^router openfabric {domain}', stop_pattern='^exit', remove_stop_mark=True) + + for interface in openfabric['interfaces_all']: + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) + + if 'frr_fabricd_config' in openfabric: + frr_cfg.add_before(frr.default_add_before, openfabric['frr_fabricd_config']) + + frr_cfg.commit_configuration(openfabric_daemon) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index 52d0b7cda..a84572f83 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -19,11 +19,13 @@ import os from sys import exit from time import sleep + from vyos.config import Config from vyos.configverify import verify_source_interface from vyos.configverify import verify_interface_exists from vyos.system import grub_util from vyos.template import render +from vyos.utils.cpu import get_cpus from vyos.utils.dict import dict_search from vyos.utils.file import write_file from vyos.utils.kernel import check_kmod @@ -35,6 +37,7 @@ from vyos.configdep import set_dependents from vyos.configdep import call_dependents from vyos import ConfigError from vyos import airbag + airbag.enable() curlrc_config = r'/etc/curlrc' @@ -42,10 +45,8 @@ ssh_config = r'/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf' systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' usb_autosuspend = r'/etc/udev/rules.d/40-usb-autosuspend.rules' kernel_dynamic_debug = r'/sys/kernel/debug/dynamic_debug/control' -time_format_to_locale = { - '12-hour': 'en_US.UTF-8', - '24-hour': 'en_GB.UTF-8' -} +time_format_to_locale = {'12-hour': 'en_US.UTF-8', '24-hour': 'en_GB.UTF-8'} + def get_config(config=None): if config: @@ -53,9 +54,9 @@ def get_config(config=None): else: conf = Config() base = ['system', 'option'] - options = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) + options = conf.get_config_dict( + base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True + ) if 'performance' in options: # Update IPv4/IPv6 and sysctl options after tuned applied it's settings @@ -64,6 +65,7 @@ def get_config(config=None): return options + def verify(options): if 'http_client' in options: config = options['http_client'] @@ -71,7 +73,9 @@ def verify(options): verify_interface_exists(options, config['source_interface']) if {'source_address', 'source_interface'} <= set(config): - raise ConfigError('Can not define both HTTP source-interface and source-address') + raise ConfigError( + 'Can not define both HTTP source-interface and source-address' + ) if 'source_address' in config: if not is_addr_assigned(config['source_address']): @@ -92,10 +96,20 @@ def verify(options): address = config['source_address'] interface = config['source_interface'] if not is_intf_addr_assigned(interface, address): - raise ConfigError(f'Address "{address}" not assigned on interface "{interface}"!') + raise ConfigError( + f'Address "{address}" not assigned on interface "{interface}"!' + ) + + if 'kernel' in options: + cpu_vendor = get_cpus()[0]['vendor_id'] + if 'amd_pstate_driver' in options['kernel'] and cpu_vendor != 'AuthenticAMD': + raise ConfigError( + f'AMD pstate driver cannot be used with "{cpu_vendor}" CPU!' + ) return None + def generate(options): render(curlrc_config, 'system/curlrc.j2', options) render(ssh_config, 'system/ssh_config.j2', options) @@ -107,10 +121,16 @@ def generate(options): cmdline_options.append('mitigations=off') if 'disable_power_saving' in options['kernel']: cmdline_options.append('intel_idle.max_cstate=0 processor.max_cstate=1') + if 'amd_pstate_driver' in options['kernel']: + mode = options['kernel']['amd_pstate_driver'] + cmdline_options.append( + f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}' + ) grub_util.update_kernel_cmdline_options(' '.join(cmdline_options)) return None + def apply(options): # System bootup beep beep_service = 'vyos-beep.service' @@ -149,7 +169,7 @@ def apply(options): if 'performance' in options: cmd('systemctl restart tuned.service') # wait until daemon has started before sending configuration - while (not is_systemd_service_running('tuned.service')): + while not is_systemd_service_running('tuned.service'): sleep(0.250) cmd('tuned-adm profile network-{performance}'.format(**options)) else: @@ -164,9 +184,9 @@ def apply(options): # Enable/diable root-partition-auto-resize SystemD service if 'root_partition_auto_resize' in options: - cmd('systemctl enable root-partition-auto-resize.service') + cmd('systemctl enable root-partition-auto-resize.service') else: - cmd('systemctl disable root-partition-auto-resize.service') + cmd('systemctl disable root-partition-auto-resize.service') # Time format 12|24-hour if 'time_format' in options: @@ -186,6 +206,7 @@ def apply(options): else: write_file(kernel_dynamic_debug, f'module {module} -p') + if __name__ == '__main__': try: c = get_config() diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py index 813d3a2b7..a83c8b9d8 100755 --- a/src/op_mode/restart.py +++ b/src/op_mode/restart.py @@ -25,11 +25,11 @@ from vyos.utils.commit import commit_in_progress config = ConfigTreeQuery() service_map = { - 'dhcp' : { + 'dhcp': { 'systemd_service': 'kea-dhcp4-server', 'path': ['service', 'dhcp-server'], }, - 'dhcpv6' : { + 'dhcpv6': { 'systemd_service': 'kea-dhcp6-server', 'path': ['service', 'dhcpv6-server'], }, @@ -61,24 +61,40 @@ service_map = { 'systemd_service': 'radvd', 'path': ['service', 'router-advert'], }, - 'snmp' : { + 'snmp': { 'systemd_service': 'snmpd', }, - 'ssh' : { + 'ssh': { 'systemd_service': 'ssh', }, - 'suricata' : { + 'suricata': { 'systemd_service': 'suricata', }, - 'vrrp' : { + 'vrrp': { 'systemd_service': 'keepalived', 'path': ['high-availability', 'vrrp'], }, - 'webproxy' : { + 'webproxy': { 'systemd_service': 'squid', }, } -services = typing.Literal['dhcp', 'dhcpv6', 'dns_dynamic', 'dns_forwarding', 'igmp_proxy', 'ipsec', 'mdns_repeater', 'reverse_proxy', 'router_advert', 'snmp', 'ssh', 'suricata' 'vrrp', 'webproxy'] +services = typing.Literal[ + 'dhcp', + 'dhcpv6', + 'dns_dynamic', + 'dns_forwarding', + 'igmp_proxy', + 'ipsec', + 'mdns_repeater', + 'reverse_proxy', + 'router_advert', + 'snmp', + 'ssh', + 'suricata', + 'vrrp', + 'webproxy', +] + def _verify(func): """Decorator checks if DHCP(v6) config exists""" @@ -102,13 +118,18 @@ def _verify(func): # Check if config does not exist if not config.exists(path): - raise vyos.opmode.UnconfiguredSubsystem(f'Service {human_name} is not configured!') + raise vyos.opmode.UnconfiguredSubsystem( + f'Service {human_name} is not configured!' + ) if config.exists(path + ['disable']): - raise vyos.opmode.UnconfiguredSubsystem(f'Service {human_name} is disabled!') + raise vyos.opmode.UnconfiguredSubsystem( + f'Service {human_name} is disabled!' + ) return func(*args, **kwargs) return _wrapper + @_verify def restart_service(raw: bool, name: services, vrf: typing.Optional[str]): systemd_service = service_map[name]['systemd_service'] @@ -117,6 +138,7 @@ def restart_service(raw: bool, name: services, vrf: typing.Optional[str]): else: call(f'systemctl restart "{systemd_service}.service"') + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 8841b0eca..83146f5ec 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -139,7 +139,7 @@ def _reload_config(daemon): # define program arguments cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') -cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'eigrpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pimd', 'pim6d', 'ldpd', 'babeld', 'bfdd'], required=False, nargs='*', help='select single or multiple daemons') +cmd_args_parser.add_argument('--daemon', choices=['zebra', 'staticd', 'bgpd', 'eigrpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'isisd', 'pimd', 'pim6d', 'ldpd', 'babeld', 'bfdd', 'fabricd'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() |