diff options
45 files changed, 648 insertions, 301 deletions
diff --git a/.github/workflows/add-pr-labels.yml b/.github/workflows/add-pr-labels.yml new file mode 100644 index 000000000..1723cceb0 --- /dev/null +++ b/.github/workflows/add-pr-labels.yml @@ -0,0 +1,19 @@ +--- +name: Add pull request labels + +on: + pull_request_target: + branches: + - current + - crux + - equuleus + - sagitta + +permissions: + pull-requests: write + contents: read + +jobs: + add-pr-label: + uses: vyos/.github/.github/workflows/add-pr-labels.yml@feature/T6349-reusable-workflows + secrets: inherit diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml index 0bfe972c0..c3696ea47 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -3,15 +3,12 @@ on: pull_request_target: types: [opened, reopened, ready_for_review, locked] + permissions: pull-requests: write + contents: read jobs: - # https://github.com/marketplace/actions/auto-author-assign assign-author: - runs-on: ubuntu-latest - steps: - - name: "Assign Author to PR" - uses: toshimaru/auto-author-assign@v1.6.2 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + uses: vyos/.github/.github/workflows/assign-author.yml@feature/T6349-reusable-workflows + secrets: inherit diff --git a/.github/workflows/chceck-pr-message.yml b/.github/workflows/chceck-pr-message.yml new file mode 100644 index 000000000..e7e456961 --- /dev/null +++ b/.github/workflows/chceck-pr-message.yml @@ -0,0 +1,18 @@ +--- +name: Check pull request message format + +on: + pull_request: + branches: + - current + - crux + - equuleus + +permissions: + pull-requests: write + contents: read + +jobs: + check-pr-title: + uses: vyos/.github/.github/workflows/check-pr-message.yml@feature/T6349-reusable-workflows + secrets: inherit diff --git a/.github/workflows/check-pr-conflicts.yml b/.github/workflows/check-pr-conflicts.yml new file mode 100644 index 000000000..0c659e6ed --- /dev/null +++ b/.github/workflows/check-pr-conflicts.yml @@ -0,0 +1,14 @@ + +name: "PR Conflicts checker" +on: + pull_request_target: + types: [synchronize] + +permissions: + pull-requests: write + contents: read + +jobs: + check-pr-conflict-call: + uses: vyos/.github/.github/workflows/check-pr-merge-conflict.yml@feature/T6349-reusable-workflows + secrets: inherit diff --git a/.github/workflows/check-stale.yml b/.github/workflows/check-stale.yml new file mode 100644 index 000000000..b5ec533f1 --- /dev/null +++ b/.github/workflows/check-stale.yml @@ -0,0 +1,13 @@ +name: "Issue and PR stale management" +on: + schedule: + - cron: "0 0 * * *" + +permissions: + pull-requests: write + contents: read + +jobs: + stale: + uses: vyos/.github/.github/workflows/check-stale.yml@feature/T6349-reusable-workflows + secrets: inherit diff --git a/.github/workflows/check-unused-imports.yml b/.github/workflows/check-unused-imports.yml new file mode 100644 index 000000000..aada264f7 --- /dev/null +++ b/.github/workflows/check-unused-imports.yml @@ -0,0 +1,15 @@ +name: Check for unused imports using Pylint +on: + pull_request: + branches: + - current + - sagitta + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-unused-imports: + uses: vyos/.github/.github/workflows/check-unused-imports.yml@feature/T6349-reusable-workflows + secrets: inherit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9e2e4bf0f..f6472784d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "Perform CodeQL Analysis" on: @@ -27,7 +16,7 @@ permissions: jobs: codeql-analysis-call: - uses: vyos/vyos-github-actions/.github/workflows/codeql-analysis.yml@current + uses: vyos/.github/.github/workflows/codeql-analysis.yml@feature/T6349-reusable-workflows secrets: inherit with: languages: "['python']" diff --git a/.github/workflows/label-backport.yml b/.github/workflows/label-backport.yml new file mode 100644 index 000000000..9192b8184 --- /dev/null +++ b/.github/workflows/label-backport.yml @@ -0,0 +1,12 @@ +name: Mergifyio backport + +on: [issue_comment] + +permissions: + pull-requests: write + contents: read + +jobs: + mergifyio-backport: + uses: vyos/.github/.github/workflows/label-backport.yml@feature/T6349-reusable-workflows + secrets: inherit diff --git a/.github/workflows/linit-j2.yml b/.github/workflows/linit-j2.yml new file mode 100644 index 000000000..364a65a14 --- /dev/null +++ b/.github/workflows/linit-j2.yml @@ -0,0 +1,18 @@ +--- +name: J2 Lint + +on: + pull_request: + branches: + - current + - crux + - equuleus + +permissions: + pull-requests: write + contents: read + +jobs: + j2lint: + uses: vyos/.github/.github/workflows/lint-j2.yml@feature/T6349-reusable-workflows + secrets: inherit diff --git a/.github/workflows/mergifyio_backport.yml b/.github/workflows/mergifyio_backport.yml deleted file mode 100644 index d9f863d9a..000000000 --- a/.github/workflows/mergifyio_backport.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Mergifyio backport - -on: [issue_comment] - -jobs: - mergifyio_backport: - if: github.repository == 'vyos/vyos-1x' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: actions-ecosystem/action-regex-match@v2 - id: regex-match - with: - text: ${{ github.event.comment.body }} - regex: '@[Mm][Ee][Rr][Gg][Ii][Ff][Yy][Ii][Oo] backport ' - - - uses: actions-ecosystem/action-add-labels@v1 - if: ${{ steps.regex-match.outputs.match != '' }} - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - labels: backport diff --git a/.github/workflows/pr-conflicts.yml b/.github/workflows/pr-conflicts.yml deleted file mode 100644 index 2fd0bb42d..000000000 --- a/.github/workflows/pr-conflicts.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: "PR Conflicts checker" -on: - pull_request_target: - types: [synchronize] - -jobs: - Conflict_Check: - name: 'Check PR status: conflicts and resolution' - runs-on: ubuntu-latest - steps: - - name: check if PRs are dirty - uses: eps1lon/actions-label-merge-conflict@v3 - with: - dirtyLabel: "state: conflict" - removeOnDirtyLabel: "state: conflict resolved" - repoToken: "${{ secrets.GITHUB_TOKEN }}" - commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request." - commentOnClean: "Conflicts have been resolved. A maintainer will review the pull request shortly." diff --git a/.github/workflows/pull-request-labels.yml b/.github/workflows/pull-request-labels.yml deleted file mode 100644 index 43856beaa..000000000 --- a/.github/workflows/pull-request-labels.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Add pull request labels - -on: - pull_request_target: - branches: - - current - - crux - - equuleus - - sagitta - -jobs: - add-pr-label: - name: Add PR Labels - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/labeler@v5 diff --git a/.github/workflows/pull-request-management.yml b/.github/workflows/pull-request-management.yml deleted file mode 100644 index 3a855c107..000000000 --- a/.github/workflows/pull-request-management.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Build Pull Request Package - -on: - pull_request: - branches: - - current - - crux - - equuleus - -jobs: - j2lint: - name: Validate j2 files - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - timeout-minutes: 2 - - name: Setup J2Lint - timeout-minutes: 2 - run: | - sudo pip install git+https://github.com/aristanetworks/j2lint.git@341b5d5db86e095b622f09770cb6367a1583620e - - name: Run J2lint - timeout-minutes: 2 - run: | - j2lint $GITHUB_WORKSPACE/data diff --git a/.github/workflows/pull-request-message-check.yml b/.github/workflows/pull-request-message-check.yml deleted file mode 100644 index 8c206a5ab..000000000 --- a/.github/workflows/pull-request-message-check.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Check pull request message format - -on: - pull_request: - branches: - - current - - crux - - equuleus - -jobs: - check-pr-title: - name: Check pull request title - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - timeout-minutes: 2 - - name: Install the requests library - run: pip3 install requests - - name: Check the PR title - timeout-minutes: 2 - run: | - ./scripts/check-pr-title-and-commit-messages.py '${{ github.event.pull_request.url }}' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index d21d151f7..000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "Issue and PR stale management" -on: - schedule: - - cron: "0 0 * * *" - -jobs: - stale: - runs-on: ubuntu-latest - if: github.repository == 'vyos/vyos-1x' - steps: - # Issue stale management - - uses: actions/stale@v6 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-stale: 90 - days-before-close: -1 - stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. The issue will be reviewed by a maintainer and may be closed' - stale-issue-label: 'state: stale' - exempt-issue-labels: 'state: accepted, state: in-progress' - stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. The PR will be reviewed by a maintainer and may be closed' - stale-pr-label: 'state: stale' - exempt-pr-labels: 'state: accepted, state: in-progress' diff --git a/.github/workflows/unused-imports.yml b/.github/workflows/unused-imports.yml deleted file mode 100644 index da57bd270..000000000 --- a/.github/workflows/unused-imports.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Check for unused imports using Pylint -on: - pull_request_target: - branches: - - current - - sagitta - -jobs: - Check-Unused-Imports: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: 3.11 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - - name: Analysing the code with pylint - run: make unused-imports diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..191394298 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @vyos/reviewers
\ No newline at end of file diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index a4ed2bcf4..c14133127 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -3,12 +3,14 @@ "bgp.py", "bonding.py", "bridge.py", +"cgnat.py", "config_mgmt.py", "conntrack.py", "container.py", "cpu.py", "dhcp.py", "dns.py", +"evpn.py", "interfaces.py", "ipsec.py", "lldp.py", diff --git a/data/templates/accel-ppp/config_chap_secrets_radius.j2 b/data/templates/accel-ppp/config_chap_secrets_radius.j2 index 595e3a565..e343ce461 100644 --- a/data/templates/accel-ppp/config_chap_secrets_radius.j2 +++ b/data/templates/accel-ppp/config_chap_secrets_radius.j2 @@ -5,7 +5,20 @@ chap-secrets={{ chap_secrets_file }} [radius] verbose=1 {% for server, options in authentication.radius.server.items() if not options.disable is vyos_defined %} -server={{ server }},{{ options.key }},auth-port={{ options.port }},acct-port={{ options.acct_port }},req-limit=0,fail-time={{ options.fail_time }} +{% set _server_cfg = "server=" %} +{% set _server_cfg = _server_cfg + server %} +{% set _server_cfg = _server_cfg + "," + options.key %} +{% set _server_cfg = _server_cfg + ",auth-port=" + options.port %} +{% set _server_cfg = _server_cfg + ",acct-port=" + options.acct_port %} +{% set _server_cfg = _server_cfg + ",req-limit=0" %} +{% set _server_cfg = _server_cfg + ",fail-time=" + options.fail_time %} +{% if options.priority is vyos_defined %} +{% set _server_cfg = _server_cfg + ",weight=" + options.priority %} +{% endif %} +{% if options.backup is vyos_defined %} +{% set _server_cfg = _server_cfg + ",backup" %} +{% endif %} +{{ _server_cfg }} {% endfor %} {% if authentication.radius.accounting_interim_interval is vyos_defined %} acct-interim-interval={{ authentication.radius.accounting_interim_interval }} diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index e7dacea36..2296a3e9e 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -15,9 +15,15 @@ <constraintErrorMessage>Container name must be alphanumeric and can contain hyphens</constraintErrorMessage> </properties> <children> + <leafNode name="allow-host-pid"> + <properties> + <help>Allow sharing host process namespace with container</help> + <valueless/> + </properties> + </leafNode> <leafNode name="allow-host-networks"> <properties> - <help>Allow host networks in container</help> + <help>Allow sharing host networking with container</help> <valueless/> </properties> </leafNode> diff --git a/interface-definitions/include/accel-ppp/radius-additions.xml.i b/interface-definitions/include/accel-ppp/radius-additions.xml.i index 3c2eb09eb..5222ba864 100644 --- a/interface-definitions/include/accel-ppp/radius-additions.xml.i +++ b/interface-definitions/include/accel-ppp/radius-additions.xml.i @@ -57,6 +57,13 @@ </properties> <defaultValue>0</defaultValue> </leafNode> + #include <include/radius-priority.xml.i> + <leafNode name="backup"> + <properties> + <help>Use backup server if other servers are not available</help> + <valueless/> + </properties> + </leafNode> </children> </tagNode> <leafNode name="timeout"> diff --git a/interface-definitions/include/radius-priority.xml.i b/interface-definitions/include/radius-priority.xml.i new file mode 100644 index 000000000..f77f5016e --- /dev/null +++ b/interface-definitions/include/radius-priority.xml.i @@ -0,0 +1,14 @@ +<!-- include start from radius-priority.xml.i --> +<leafNode name="priority"> + <properties> + <help>Server priority</help> + <valueHelp> + <format>u32:1-255</format> + <description>Server priority</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/nat_cgnat.xml.in b/interface-definitions/nat_cgnat.xml.in index caa26b4d9..fce5e655d 100644 --- a/interface-definitions/nat_cgnat.xml.in +++ b/interface-definitions/nat_cgnat.xml.in @@ -123,6 +123,7 @@ <validator name="ipv4-host"/> <validator name="ipv4-range"/> </constraint> + <multi/> </properties> </leafNode> </children> diff --git a/interface-definitions/system_login.xml.in b/interface-definitions/system_login.xml.in index e94bb7219..f6c8021d3 100644 --- a/interface-definitions/system_login.xml.in +++ b/interface-definitions/system_login.xml.in @@ -202,17 +202,8 @@ <tagNode name="server"> <children> #include <include/radius-timeout.xml.i> + #include <include/radius-priority.xml.i> <leafNode name="priority"> - <properties> - <help>Server priority</help> - <valueHelp> - <format>u32:1-255</format> - <description>Server priority</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-255"/> - </constraint> - </properties> <defaultValue>255</defaultValue> </leafNode> </children> diff --git a/op-mode-definitions/include/vni-tagnode-all.xml.i b/op-mode-definitions/include/vni-tagnode-all.xml.i index 0fedb9371..fabab19d7 100644 --- a/op-mode-definitions/include/vni-tagnode-all.xml.i +++ b/op-mode-definitions/include/vni-tagnode-all.xml.i @@ -3,9 +3,10 @@ <properties> <help>VXLAN network identifier (VNI) number</help> <completionHelp> - <list>1-16777215 all</list> + <list><1-16777215> all</list> + <script>${vyos_completion_dir}/list_vni.sh</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> </tagNode> <!-- included end --> diff --git a/op-mode-definitions/include/vni-tagnode.xml.i b/op-mode-definitions/include/vni-tagnode.xml.i index 22f2d33bd..f5b99dcc8 100644 --- a/op-mode-definitions/include/vni-tagnode.xml.i +++ b/op-mode-definitions/include/vni-tagnode.xml.i @@ -3,9 +3,10 @@ <properties> <help>VXLAN network identifier (VNI) number</help> <completionHelp> - <list>1-16777215</list> + <list><1-16777215></list> + <script>${vyos_completion_dir}/list_vni.sh</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> </tagNode> <!-- included end --> diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in index 307a91337..6398c0e07 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -7,6 +7,19 @@ <help>Show IPv4 Network Address Translation (NAT) information</help> </properties> <children> + <node name="cgnat"> + <properties> + <help>Show Carrier-Grade Network Address Translation (CGNAT)</help> + </properties> + <children> + <node name="allocation"> + <properties> + <help>Show allocated CGNAT parameters</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/cgnat.py show_allocation</command> + </node> + </children> + </node> <node name="source"> <properties> <help>Show source IPv4 to IPv4 Network Address Translation (NAT) information</help> diff --git a/op-mode-definitions/show-evpn.xml.in b/op-mode-definitions/show-evpn.xml.in index a005cbc30..3c1e5c7d6 100644 --- a/op-mode-definitions/show-evpn.xml.in +++ b/op-mode-definitions/show-evpn.xml.in @@ -14,7 +14,7 @@ <children> #include <include/frr-detail.xml.i> </children> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> </node> <tagNode name="access-vlan"> <properties> @@ -31,7 +31,7 @@ <list><1-4094></list> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> </node> </children> </tagNode> @@ -43,6 +43,45 @@ #include <include/vni-tagnode-all.xml.i> </children> </node> + <tagNode name="es"> + <properties> + <help>Show ESI information for specified ESI</help> + <completionHelp> + <list><esi></list> + <script>${vyos_completion_dir}/list_esi.sh</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> + </tagNode> + <node name="es"> + <properties> + <help>Show ESI information</help> + </properties> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show ESI details</help> + </properties> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> + </leafNode> + </children> + </node> + <node name="es-evi"> + <properties> + <help>Show ESI information per EVI</help> + </properties> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show ESI per EVI details</help> + </properties> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> + </leafNode> + #include <include/vni-tagnode.xml.i> + </children> + </node> <node name="mac"> <properties> <help>MAC addresses</help> @@ -67,7 +106,23 @@ #include <include/vni-tagnode-all.xml.i> </children> </node> + #include <include/vni-tagnode.xml.i> + <node name="vni"> + <properties> + <help>Show VNI information</help> + </properties> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show VNI details</help> + </properties> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> + </leafNode> + </children> + </node> </children> + <command>${vyos_op_scripts_dir}/evpn.py show_evpn --command "$*"</command> </node> </children> </node> diff --git a/python/vyos/system/image.py b/python/vyos/system/image.py index ba9a6dfa7..aae52e770 100644 --- a/python/vyos/system/image.py +++ b/python/vyos/system/image.py @@ -18,8 +18,9 @@ from re import compile as re_compile from functools import wraps from tempfile import TemporaryDirectory from typing import TypedDict +from json import loads -from vyos import version +from vyos.defaults import directories from vyos.system import disk, grub # Define variables @@ -201,9 +202,12 @@ def get_running_image() -> str: if running_image_result: running_image: str = running_image_result.groupdict().get( 'image_version', '') - # we need to have a fallback for live systems + # we need to have a fallback for live systems: + # explicit read from version file if not running_image: - running_image: str = version.get_version() + json_data: str = Path(directories['data']).joinpath('version.json').read_text() + dict_data: dict = loads(json_data) + running_image: str = dict_data['version'] return running_image diff --git a/python/vyos/version.py b/python/vyos/version.py index b5ed2705b..86e96d0ec 100644 --- a/python/vyos/version.py +++ b/python/vyos/version.py @@ -33,11 +33,11 @@ import os import requests import vyos.defaults +from vyos.system.image import is_live_boot from vyos.utils.file import read_file from vyos.utils.file import read_json from vyos.utils.process import popen -from vyos.utils.process import run from vyos.utils.process import DEVNULL version_file = os.path.join(vyos.defaults.directories['data'], 'version.json') @@ -81,16 +81,14 @@ def get_full_version_data(fname=version_file): else: version_data['system_type'] = f"{hypervisor} guest" - # Get boot type, it can be livecd, installed image, or, possible, a system installed - # via legacy "install system" mechanism + # Get boot type, it can be livecd or installed image # In installed images, the squashfs image file is named after its image version, # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot # from an installed image - boot_via = "installed image" - if run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0: + if is_live_boot(): boot_via = "livecd" - elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0: - boot_via = "legacy non-image installation" + else: + boot_via = "installed image" version_data['boot_via'] = boot_via # Get hardware details from DMI diff --git a/scripts/check-pr-title-and-commit-messages.py b/scripts/check-pr-title-and-commit-messages.py deleted file mode 100755 index 001f6cf82..000000000 --- a/scripts/check-pr-title-and-commit-messages.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 - -import re -import sys -import time - -import requests - -# Use the same regex for PR title and commit messages for now -title_regex = r'^(([a-zA-Z\-_.]+:\s)?)T\d+:\s+[^\s]+.*' -commit_regex = title_regex - -def check_pr_title(title): - if not re.match(title_regex, title): - print("PR title '{}' does not match the required format!".format(title)) - print("Valid title example: T99999: make IPsec secure") - sys.exit(1) - -def check_commit_message(title): - if not re.match(commit_regex, title): - print("Commit title '{}' does not match the required format!".format(title)) - print("Valid title example: T99999: make IPsec secure") - sys.exit(1) - -if __name__ == '__main__': - if len(sys.argv) < 2: - print("Please specify pull request URL!") - sys.exit(1) - - # There seems to be a race condition that causes this scripts to receive - # an incomplete PR object that is missing certain fields, - # which causes temporary CI failures that require re-running the script - # - # It's probably better to add a small delay to prevent that - time.sleep(5) - - # Get the pull request object - pr = requests.get(sys.argv[1]).json() - if "title" not in pr: - print("The PR object does not have a title field!") - print("Did not receive a valid pull request object, please check the URL!") - sys.exit(1) - - check_pr_title(pr["title"]) - - # Get the list of commits - commits = requests.get(pr["commits_url"]).json() - for c in commits: - # Retrieve every individual commit and check its title - co = requests.get(c["url"]).json() - check_commit_message(co["commit"]["message"]) diff --git a/smoketest/config-tests/container-simple b/smoketest/config-tests/container-simple index 299af64cb..cc80ef4cf 100644 --- a/smoketest/config-tests/container-simple +++ b/smoketest/config-tests/container-simple @@ -8,5 +8,6 @@ set container name c01 capability 'net-bind-service' set container name c01 capability 'net-raw' set container name c01 image 'busybox:stable' set container name c02 allow-host-networks +set container name c02 allow-host-pid set container name c02 capability 'sys-time' set container name c02 image 'busybox:stable' diff --git a/smoketest/configs/container-simple b/smoketest/configs/container-simple index 05efe05e9..82983afb7 100644 --- a/smoketest/configs/container-simple +++ b/smoketest/configs/container-simple @@ -7,6 +7,7 @@ container { } name c02 { allow-host-networks + allow-host-pid cap-add sys-time image busybox:stable } diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py index 383adc445..ab723e707 100644 --- a/smoketest/scripts/cli/base_accel_ppp_test.py +++ b/smoketest/scripts/cli/base_accel_ppp_test.py @@ -367,6 +367,27 @@ class BasicAccelPPPTest: ] ) + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "backup", + ] + ) + + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "priority", + "10", + ] + ) + # commit changes self.cli_commit() @@ -379,6 +400,8 @@ class BasicAccelPPPTest: self.assertEqual(f"acct-port=0", server[3]) self.assertEqual(f"req-limit=0", server[4]) self.assertEqual(f"fail-time=0", server[5]) + self.assertIn('weight=10', server) + self.assertIn('backup', server) def test_accel_ipv4_pool(self): self.basic_config(is_gateway=False, is_client_pool=False) diff --git a/smoketest/scripts/cli/test_cgnat.py b/smoketest/scripts/cli/test_cgnat.py new file mode 100755 index 000000000..c65c58820 --- /dev/null +++ b/smoketest/scripts/cli/test_cgnat.py @@ -0,0 +1,99 @@ +#!/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 os +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError + + +base_path = ['nat', 'cgnat'] +nftables_cgnat_config = '/run/nftables-cgnat.nft' + + +class TestCGNAT(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestCGNAT, cls).setUpClass() + + # 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() + self.assertFalse(os.path.exists(nftables_cgnat_config)) + + def test_cgnat(self): + internal_name = 'vyos-int-01' + external_name = 'vyos-ext-01' + internal_net = '100.64.0.0/29' + external_net = '192.0.2.1-192.0.2.2' + external_ports = '40000-60000' + ports_per_subscriber = '5000' + rule = '100' + + nftables_search = [ + ['map tcp_nat_map'], + ['map udp_nat_map'], + ['map icmp_nat_map'], + ['map other_nat_map'], + ['100.64.0.0 : 192.0.2.1 . 40000-44999'], + ['100.64.0.1 : 192.0.2.1 . 45000-49999'], + ['100.64.0.2 : 192.0.2.1 . 50000-54999'], + ['100.64.0.3 : 192.0.2.1 . 55000-59999'], + ['100.64.0.4 : 192.0.2.2 . 40000-44999'], + ['100.64.0.5 : 192.0.2.2 . 45000-49999'], + ['100.64.0.6 : 192.0.2.2 . 50000-54999'], + ['100.64.0.7 : 192.0.2.2 . 55000-59999'], + ['chain POSTROUTING'], + ['type nat hook postrouting priority srcnat'], + ['ip protocol tcp counter snat ip to ip saddr map @tcp_nat_map'], + ['ip protocol udp counter snat ip to ip saddr map @udp_nat_map'], + ['ip protocol icmp counter snat ip to ip saddr map @icmp_nat_map'], + ['counter snat ip to ip saddr map @other_nat_map'], + ] + + self.cli_set(base_path + ['pool', 'external', external_name, 'external-port-range', external_ports]) + self.cli_set(base_path + ['pool', 'external', external_name, 'range', external_net]) + + # allocation out of the available ports + with self.assertRaises(ConfigSessionError): + self.cli_set(base_path + ['pool', 'external', external_name, 'per-user-limit', 'port', '8000']) + self.cli_commit() + self.cli_set(base_path + ['pool', 'external', external_name, 'per-user-limit', 'port', ports_per_subscriber]) + + # internal pool not set + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['pool', 'internal', internal_name, 'range', internal_net]) + + self.cli_set(base_path + ['rule', rule, 'source', 'pool', internal_name]) + # non-exist translation pool + with self.assertRaises(ConfigSessionError): + self.cli_set(base_path + ['rule', rule, 'translation', 'pool', 'fake-pool']) + self.cli_commit() + + self.cli_set(base_path + ['rule', rule, 'translation', 'pool', external_name]) + self.cli_commit() + + self.verify_nftables(nftables_search, 'ip cgnat', inverse=False, args='-s') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 1b9cc50fe..585c1dc89 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -16,6 +16,7 @@ import unittest +from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError @@ -480,6 +481,8 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): # Commit main OSPF changes self.cli_commit() + sleep(10) + # Verify main OSPF changes frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) self.assertIn(f'router ospf', frrconfig) diff --git a/smoketest/scripts/cli/test_vpn_l2tp.py b/smoketest/scripts/cli/test_vpn_l2tp.py index 8c4e53895..07a7e2906 100755 --- a/smoketest/scripts/cli/test_vpn_l2tp.py +++ b/smoketest/scripts/cli/test_vpn_l2tp.py @@ -95,6 +95,29 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): self.cli_set(base_path + ['authentication', 'protocols', 'chap']) self.cli_commit() + def test_l2tp_radius_server(self): + base_path = ['vpn', 'l2tp', 'remote-access'] + radius_server = "192.0.2.22" + radius_key = "secretVyOS" + + self.cli_set(base_path + ['authentication', 'mode', 'radius']) + self.cli_set(base_path + ['gateway-address', '192.0.2.1']) + self.cli_set(base_path + ['client-ip-pool', 'SIMPLE-POOL', 'range', '192.0.2.0/24']) + self.cli_set(base_path + ['default-pool', 'SIMPLE-POOL']) + self.cli_set(base_path + ['authentication', 'radius', 'server', radius_server, 'key', radius_key]) + self.cli_set(base_path + ['authentication', 'radius', 'server', radius_server, 'priority', '10']) + self.cli_set(base_path + ['authentication', 'radius', 'server', radius_server, 'backup']) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True) + conf.read(self._config_file) + server = conf["radius"]["server"].split(",") + self.assertIn('weight=10', server) + self.assertIn('backup', server) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/completion/list_esi.sh b/src/completion/list_esi.sh new file mode 100755 index 000000000..b8373fa57 --- /dev/null +++ b/src/completion/list_esi.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# +# 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/>. +# +# This script is completion helper to list all valid ESEs that are visible to FRR + +esiJson=$(vtysh -c 'show evpn es json') +echo "$(echo "$esiJson" | jq -r '.[] | .esi')" diff --git a/src/completion/list_vni.sh b/src/completion/list_vni.sh new file mode 100755 index 000000000..f8bd4a993 --- /dev/null +++ b/src/completion/list_vni.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# +# 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/>. +# +# This script is completion helper to list all configured VNIs that are visible to FRR + +vniJson=$(vtysh -c 'show evpn vni json') +echo "$(echo "$vniJson" | jq -r 'keys | .[]')" diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index a73a18ffa..91a10e891 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -329,9 +329,13 @@ def generate_run_arguments(name, container_config): prop = vol_config['propagation'] volume += f' --volume {svol}:{dvol}:{mode},{prop}' + host_pid = '' + if 'allow_host_pid' in container_config: + host_pid = '--pid host' + container_base_cmd = f'--detach --interactive --tty --replace {capabilities} ' \ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ - f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid}' + f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid} {host_pid}' entrypoint = '' if 'entrypoint' in container_config: @@ -339,11 +343,6 @@ def generate_run_arguments(name, container_config): entrypoint = json_write(container_config['entrypoint'].split()).replace('"', """) entrypoint = f'--entrypoint '{entrypoint}'' - hostname = '' - if 'host_name' in container_config: - hostname = container_config['host_name'] - hostname = f'--hostname {hostname}' - command = '' if 'command' in container_config: command = container_config['command'].strip() diff --git a/src/conf_mode/nat_cgnat.py b/src/conf_mode/nat_cgnat.py index f41d66c66..5ad65de80 100755 --- a/src/conf_mode/nat_cgnat.py +++ b/src/conf_mode/nat_cgnat.py @@ -189,11 +189,6 @@ def verify(config): if 'rule' not in config: raise ConfigError(f'Rule must be defined!') - # As PoC allow only one rule for CGNAT translations - # one internal pool and one external pool - if len(config['rule']) > 1: - raise ConfigError(f'Only one rule is allowed for translations!') - for pool in ('external', 'internal'): if pool not in config['pool']: raise ConfigError(f'{pool} pool must be defined!') @@ -203,6 +198,13 @@ def verify(config): f'Range for "{pool} pool {pool_name}" must be defined!' ) + external_pools_query = "keys(pool.external)" + external_pools: list = jmespath.search(external_pools_query, config) + internal_pools_query = "keys(pool.internal)" + internal_pools: list = jmespath.search(internal_pools_query, config) + + used_external_pools = {} + used_internal_pools = {} for rule, rule_config in config['rule'].items(): if 'source' not in rule_config: raise ConfigError(f'Rule "{rule}" source pool must be defined!') @@ -212,49 +214,82 @@ def verify(config): if 'translation' not in rule_config: raise ConfigError(f'Rule "{rule}" translation pool must be defined!') + # Check if pool exists + internal_pool = rule_config['source']['pool'] + if internal_pool not in internal_pools: + raise ConfigError(f'Internal pool "{internal_pool}" does not exist!') + external_pool = rule_config['translation']['pool'] + if external_pool not in external_pools: + raise ConfigError(f'External pool "{external_pool}" does not exist!') + + # Check pool duplication in different rules + if external_pool in used_external_pools: + raise ConfigError( + f'External pool "{external_pool}" is already used in rule ' + f'{used_external_pools[external_pool]} and cannot be used in ' + f'rule {rule}!' + ) + + if internal_pool in used_internal_pools: + raise ConfigError( + f'Internal pool "{internal_pool}" is already used in rule ' + f'{used_internal_pools[internal_pool]} and cannot be used in ' + f'rule {rule}!' + ) + + used_external_pools[external_pool] = rule + used_internal_pools[internal_pool] = rule + def generate(config): if not config: return None - # first external pool as we allow only one as PoC - ext_pool_name = jmespath.search("rule.*.translation | [0]", config).get('pool') - int_pool_name = jmespath.search("rule.*.source | [0]", config).get('pool') - ext_query = f"pool.external.{ext_pool_name}.range | keys(@)" - int_query = f"pool.internal.{int_pool_name}.range" - external_ranges = jmespath.search(ext_query, config) - internal_ranges = [jmespath.search(int_query, config)] - - external_list_count = [] - external_list_hosts = [] - internal_list_count = [] - internal_list_hosts = [] - for ext_range in external_ranges: - # External hosts count - e_count = IPOperations(ext_range).get_ips_count() - external_list_count.append(e_count) - # External hosts list - e_hosts = IPOperations(ext_range).convert_prefix_to_list_ips() - external_list_hosts.extend(e_hosts) - for int_range in internal_ranges: - # Internal hosts count - i_count = IPOperations(int_range).get_ips_count() - internal_list_count.append(i_count) - # Internal hosts list - i_hosts = IPOperations(int_range).convert_prefix_to_list_ips() - internal_list_hosts.extend(i_hosts) - - external_host_count = sum(external_list_count) - internal_host_count = sum(internal_list_count) - ports_per_user = int( - jmespath.search(f'pool.external.{ext_pool_name}.per_user_limit.port', config) - ) - external_port_range: str = jmespath.search( - f'pool.external.{ext_pool_name}.external_port_range', config - ) - proto_maps, other_maps = generate_port_rules( - external_list_hosts, internal_list_hosts, ports_per_user, external_port_range - ) + proto_maps = [] + other_maps = [] + + for rule, rule_config in config['rule'].items(): + ext_pool_name: str = rule_config['translation']['pool'] + int_pool_name: str = rule_config['source']['pool'] + + external_ranges: list = [range for range in config['pool']['external'][ext_pool_name]['range']] + internal_ranges: list = [range for range in config['pool']['internal'][int_pool_name]['range']] + external_list_hosts_count = [] + external_list_hosts = [] + internal_list_hosts_count = [] + internal_list_hosts = [] + + for ext_range in external_ranges: + # External hosts count + e_count = IPOperations(ext_range).get_ips_count() + external_list_hosts_count.append(e_count) + # External hosts list + e_hosts = IPOperations(ext_range).convert_prefix_to_list_ips() + external_list_hosts.extend(e_hosts) + + for int_range in internal_ranges: + # Internal hosts count + i_count = IPOperations(int_range).get_ips_count() + internal_list_hosts_count.append(i_count) + # Internal hosts list + i_hosts = IPOperations(int_range).convert_prefix_to_list_ips() + internal_list_hosts.extend(i_hosts) + + external_host_count = sum(external_list_hosts_count) + internal_host_count = sum(internal_list_hosts_count) + ports_per_user = int( + jmespath.search(f'pool.external."{ext_pool_name}".per_user_limit.port', config) + ) + external_port_range: str = jmespath.search( + f'pool.external."{ext_pool_name}".external_port_range', config + ) + + rule_proto_maps, rule_other_maps = generate_port_rules( + external_list_hosts, internal_list_hosts, ports_per_user, external_port_range + ) + + proto_maps.extend(rule_proto_maps) + other_maps.extend(rule_other_maps) config['proto_map_elements'] = ', '.join(proto_maps) config['other_map_elements'] = ', '.join(other_maps) diff --git a/src/op_mode/cgnat.py b/src/op_mode/cgnat.py new file mode 100755 index 000000000..e58b15809 --- /dev/null +++ b/src/op_mode/cgnat.py @@ -0,0 +1,74 @@ +#!/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 json +import sys + +from tabulate import tabulate + +import vyos.opmode + +from vyos.configquery import ConfigTreeQuery +from vyos.utils.process import cmd + +CGNAT_TABLE = 'cgnat' + + +def _get_raw_data(): + """ Get CGNAT dictionary + """ + cmd_output = cmd(f'nft --json list table ip {CGNAT_TABLE}') + data = json.loads(cmd_output) + return data + + +def _get_formatted_output(data): + elements = data['nftables'][2]['map']['elem'] + allocations = [] + for elem in elements: + internal = elem[0] # internal + external = elem[1]['concat'][0] # external + start_port = elem[1]['concat'][1]['range'][0] + end_port = elem[1]['concat'][1]['range'][1] + port_range = f'{start_port}-{end_port}' + allocations.append((internal, external, port_range)) + + headers = ['Internal IP', 'External IP', 'Port range'] + output = tabulate(allocations, headers, numalign="left") + return output + + +def show_allocation(raw: bool): + config = ConfigTreeQuery() + if not config.exists('nat cgnat'): + raise vyos.opmode.UnconfiguredSubsystem('CGNAT is not configured') + + if raw: + return _get_raw_data() + + else: + raw_data = _get_raw_data() + return _get_formatted_output(raw_data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/evpn.py b/src/op_mode/evpn.py new file mode 100644 index 000000000..cae4ab9f5 --- /dev/null +++ b/src/op_mode/evpn.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2016-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/>. +# +# This script is a helper to run VTYSH commands for "show evpn", allowing for the --raw flag to output JSON + +import sys +import typing +import json + +import vyos.opmode +from vyos.utils.process import cmd + +def show_evpn(raw: bool, command: typing.Optional[str]): + if raw: + command = f"{command} json" + evpnDict = {} + try: + evpnDict['evpn'] = json.loads(cmd(f"vtysh -c '{command}'")) + except: + raise vyos.opmode.DataUnavailable(f"\"{command.replace(' json', '')}\" is invalid or has no JSON option") + + return evpnDict + else: + return cmd(f"vtysh -c '{command}'") + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 2bc7e24fe..4ab524fb7 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -263,7 +263,7 @@ def _get_formatted_translation(dict_data, nat_direction, family, verbose): proto = meta['layer4']['protoname'] if direction == 'independent': conn_id = meta['id'] - timeout = meta['timeout'] + timeout = meta.get('timeout', 'n/a') orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src diff --git a/src/op_mode/version.py b/src/op_mode/version.py index ad0293aca..09d69ad1d 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2016-2022 VyOS maintainers and contributors +# Copyright (C) 2016-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 @@ -30,11 +30,15 @@ from jinja2 import Template version_output_tmpl = """ Version: VyOS {{version}} Release train: {{release_train}} +Release flavor: {{flavor}} Built by: {{built_by}} Built on: {{built_on}} Build UUID: {{build_uuid}} Build commit ID: {{build_git}} +{%- if build_comment %} +Build comment: {{build_comment}} +{% endif %} Architecture: {{system_arch}} Boot via: {{boot_via}} |