diff options
134 files changed, 2307 insertions, 2047 deletions
diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index e0b9ee430..000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,12 +0,0 @@ -equuleus: -  - any: -    - base-branch: 'equuleus' -current: -  - any: -    - base-branch: 'current' -crux: -  - any: -    - base-branch: 'crux' -sagitta: -  - any: -    - base-branch: 'sagitta' diff --git a/.github/reviewers.yml b/.github/reviewers.yml deleted file mode 100644 index a1647d20d..000000000 --- a/.github/reviewers.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -"**/*": -  - team: reviewers 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 1a7f8ef0b..c3696ea47 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -3,25 +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 }} - -  # https://github.com/shufo/auto-assign-reviewer-by-files -  assign_reviewer: -    runs-on: ubuntu-latest -    steps: -      - name: Request review based on files changes and/or groups the author belongs to -        uses: shufo/auto-assign-reviewer-by-files@v1.1.4 -        with: -          token: ${{ secrets.PR_ACTION_ASSIGN_REVIEWERS }} -          config: .github/reviewers.yml +    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 c39800ac8..f6472784d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,74 +1,22 @@ -# 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: "CodeQL" +name: "Perform CodeQL Analysis"  on:    push: -    branches: [ "current", crux, equuleus ] +    branches: [ "current", "sagitta", "equuleus" ]    pull_request:      # The branches below must be a subset of the branches above      branches: [ "current" ]    schedule:      - cron: '22 10 * * 0' -jobs: -  analyze: -    name: Analyze -    runs-on: ubuntu-latest -    permissions: -      actions: read -      contents: read -      security-events: write - -    strategy: -      fail-fast: false -      matrix: -        language: [ 'python' ] -        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] -        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - -    steps: -    - name: Checkout repository -      uses: actions/checkout@v3 - -    # Initializes the CodeQL tools for scanning. -    - name: Initialize CodeQL -      uses: github/codeql-action/init@v2 -      with: -        languages: ${{ matrix.language }} -        # If you wish to specify custom queries, you can do so here or in a config file. -        # By default, queries listed here will override any specified in a config file. -        # Prefix the list here with "+" to use these queries and those in the config file. -         -        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs -        # queries: security-extended,security-and-quality - -         -    # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java). -    # If this step fails, then you should remove it and run the build manually (see below) -    - name: Autobuild -      uses: github/codeql-action/autobuild@v2 +permissions: +  actions: read +  contents: read +  security-events: write -    # ℹ️ Command-line programs to run using the OS shell. -    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - -    #   If the Autobuild fails above, remove it and uncomment the following three lines.  -    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - -    # - run: | -    #   echo "Run, Build Application using script" -    #   ./location_of_script_within_repo/buildscript.sh - -    - name: Perform CodeQL Analysis -      uses: github/codeql-action/analyze@v2 -      with: -        category: "/language:${{matrix.language}}" +jobs: +  codeql-analysis-call: +    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/.gitignore b/.gitignore index 7707e94ca..507daceee 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,7 @@ data/component-versions.json  # vyos-1x XML cache  python/vyos/xml_ref/cache.py  python/vyos/xml_ref/pkg_cache/*_cache.py + +# We do not use pip +Pipfile +Pipfile.lock 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/Pipfile b/Pipfile deleted file mode 100644 index 5a4fb6fe9..000000000 --- a/Pipfile +++ /dev/null @@ -1,18 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -lxml = "*" -pylint = "*" -nose = "*" -coverage = "*" - -[packages] -vyos = {file = "./python"} -jinja2 = "*" -paramiko = "*" - -[requires] -python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 5a048becc..000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,549 +0,0 @@ -{ -    "_meta": { -        "hash": { -            "sha256": "2ec6c667a8fe2b96d82639414919deb0462744211709afe93f6a538a0b15dc1f" -        }, -        "pipfile-spec": 6, -        "requires": { -            "python_version": "3.6" -        }, -        "sources": [ -            { -                "name": "pypi", -                "url": "https://pypi.org/simple", -                "verify_ssl": true -            } -        ] -    }, -    "default": { -        "bcrypt": { -            "hashes": [ -                "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f", -                "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5", -                "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb", -                "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258", -                "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4", -                "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc", -                "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2", -                "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326", -                "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483", -                "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a", -                "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966", -                "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63", -                "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c", -                "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551", -                "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d", -                "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e", -                "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0", -                "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c", -                "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb", -                "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1", -                "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42", -                "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946", -                "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab", -                "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1", -                "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c", -                "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7", -                "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369" -            ], -            "markers": "python_version >= '3.7'", -            "version": "==4.1.2" -        }, -        "cffi": { -            "hashes": [ -                "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", -                "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", -                "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", -                "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", -                "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", -                "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", -                "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", -                "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", -                "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", -                "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", -                "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", -                "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", -                "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", -                "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", -                "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", -                "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", -                "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", -                "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", -                "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", -                "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", -                "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", -                "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", -                "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", -                "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", -                "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", -                "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", -                "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", -                "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", -                "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", -                "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", -                "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", -                "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", -                "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", -                "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", -                "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", -                "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", -                "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", -                "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", -                "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", -                "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", -                "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", -                "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", -                "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", -                "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", -                "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", -                "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", -                "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", -                "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", -                "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", -                "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", -                "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", -                "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" -            ], -            "markers": "platform_python_implementation != 'PyPy'", -            "version": "==1.16.0" -        }, -        "cryptography": { -            "hashes": [ -                "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", -                "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", -                "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", -                "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", -                "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", -                "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", -                "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", -                "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", -                "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", -                "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", -                "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", -                "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", -                "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", -                "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", -                "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", -                "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", -                "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", -                "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", -                "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", -                "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", -                "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", -                "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", -                "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", -                "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", -                "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", -                "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", -                "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", -                "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", -                "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", -                "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", -                "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", -                "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" -            ], -            "markers": "python_version >= '3.7'", -            "version": "==42.0.5" -        }, -        "jinja2": { -            "hashes": [ -                "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", -                "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" -            ], -            "index": "pypi", -            "markers": "python_version >= '3.7'", -            "version": "==3.1.3" -        }, -        "markupsafe": { -            "hashes": [ -                "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", -                "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", -                "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", -                "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", -                "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", -                "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", -                "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", -                "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", -                "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", -                "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", -                "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", -                "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", -                "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", -                "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", -                "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", -                "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", -                "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", -                "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", -                "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", -                "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", -                "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", -                "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", -                "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", -                "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", -                "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", -                "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", -                "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", -                "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", -                "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", -                "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", -                "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", -                "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", -                "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", -                "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", -                "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", -                "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", -                "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", -                "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", -                "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", -                "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", -                "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", -                "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", -                "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", -                "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", -                "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", -                "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", -                "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", -                "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", -                "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", -                "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", -                "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", -                "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", -                "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", -                "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", -                "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", -                "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", -                "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", -                "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", -                "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", -                "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" -            ], -            "markers": "python_version >= '3.7'", -            "version": "==2.1.5" -        }, -        "paramiko": { -            "hashes": [ -                "sha256:43f0b51115a896f9c00f59618023484cb3a14b98bbceab43394a39c6739b7ee7", -                "sha256:aac08f26a31dc4dffd92821527d1682d99d52f9ef6851968114a8728f3c274d3" -            ], -            "index": "pypi", -            "markers": "python_version >= '3.6'", -            "version": "==3.4.0" -        }, -        "pycparser": { -            "hashes": [ -                "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", -                "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" -            ], -            "markers": "python_version >= '3.8'", -            "version": "==2.22" -        }, -        "pynacl": { -            "hashes": [ -                "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", -                "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", -                "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", -                "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", -                "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", -                "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", -                "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", -                "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", -                "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", -                "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" -            ], -            "markers": "python_version >= '3.6'", -            "version": "==1.5.0" -        }, -        "vyos": { -            "file": "./python" -        } -    }, -    "develop": { -        "astroid": { -            "hashes": [ -                "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819", -                "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4" -            ], -            "markers": "python_full_version >= '3.8.0'", -            "version": "==3.1.0" -        }, -        "coverage": { -            "hashes": [ -                "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", -                "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", -                "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", -                "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", -                "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", -                "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", -                "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", -                "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", -                "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", -                "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", -                "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", -                "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", -                "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", -                "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", -                "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", -                "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", -                "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", -                "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", -                "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", -                "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", -                "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", -                "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", -                "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", -                "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", -                "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", -                "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", -                "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", -                "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", -                "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", -                "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", -                "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", -                "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", -                "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", -                "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", -                "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", -                "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", -                "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", -                "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", -                "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", -                "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", -                "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", -                "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", -                "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", -                "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", -                "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", -                "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", -                "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", -                "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", -                "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", -                "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", -                "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", -                "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" -            ], -            "index": "pypi", -            "markers": "python_version >= '3.8'", -            "version": "==7.4.4" -        }, -        "dill": { -            "hashes": [ -                "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", -                "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7" -            ], -            "markers": "python_version >= '3.11'", -            "version": "==0.3.8" -        }, -        "isort": { -            "hashes": [ -                "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", -                "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" -            ], -            "markers": "python_full_version >= '3.8.0'", -            "version": "==5.13.2" -        }, -        "lxml": { -            "hashes": [ -                "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04", -                "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0", -                "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739", -                "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a", -                "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1", -                "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218", -                "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9", -                "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188", -                "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138", -                "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585", -                "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637", -                "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe", -                "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d", -                "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1", -                "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095", -                "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9", -                "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81", -                "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57", -                "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536", -                "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a", -                "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052", -                "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01", -                "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98", -                "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433", -                "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1", -                "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f", -                "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4", -                "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", -                "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", -                "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", -                "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5", -                "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", -                "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", -                "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", -                "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4", -                "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be", -                "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919", -                "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af", -                "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66", -                "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1", -                "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af", -                "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec", -                "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b", -                "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289", -                "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a", -                "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d", -                "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102", -                "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9", -                "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc", -                "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45", -                "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa", -                "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a", -                "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c", -                "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461", -                "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708", -                "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca", -                "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd", -                "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913", -                "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da", -                "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0", -                "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5", -                "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5", -                "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96", -                "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41", -                "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3", -                "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456", -                "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c", -                "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867", -                "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0", -                "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213", -                "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619", -                "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240", -                "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c", -                "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377", -                "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b", -                "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c", -                "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54", -                "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b", -                "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53", -                "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029", -                "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6", -                "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885", -                "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94", -                "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134", -                "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8", -                "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9", -                "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863", -                "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b", -                "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806", -                "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11", -                "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9", -                "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817", -                "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95", -                "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8", -                "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc", -                "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47", -                "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b", -                "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0", -                "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a", -                "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f", -                "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56", -                "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef", -                "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851", -                "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7", -                "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62", -                "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4", -                "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a", -                "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c", -                "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533", -                "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f", -                "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e", -                "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a", -                "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3", -                "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b", -                "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4", -                "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0", -                "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d", -                "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3", -                "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5", -                "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534", -                "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4", -                "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144", -                "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd", -                "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd", -                "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860", -                "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704", -                "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8", -                "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d", -                "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9", -                "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f", -                "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad", -                "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc", -                "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510", -                "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937", -                "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a", -                "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460", -                "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85", -                "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86", -                "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0", -                "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246", -                "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7", -                "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa", -                "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08", -                "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270", -                "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a", -                "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169", -                "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e", -                "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75", -                "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd", -                "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354", -                "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c", -                "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1", -                "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb", -                "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f", -                "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef" -            ], -            "index": "pypi", -            "markers": "python_version >= '3.6'", -            "version": "==5.2.1" -        }, -        "mccabe": { -            "hashes": [ -                "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", -                "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" -            ], -            "markers": "python_version >= '3.6'", -            "version": "==0.7.0" -        }, -        "nose": { -            "hashes": [ -                "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", -                "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", -                "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" -            ], -            "index": "pypi", -            "version": "==1.3.7" -        }, -        "platformdirs": { -            "hashes": [ -                "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", -                "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" -            ], -            "markers": "python_version >= '3.8'", -            "version": "==4.2.0" -        }, -        "pylint": { -            "hashes": [ -                "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74", -                "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23" -            ], -            "index": "pypi", -            "markers": "python_full_version >= '3.8.0'", -            "version": "==3.1.0" -        }, -        "tomlkit": { -            "hashes": [ -                "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b", -                "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3" -            ], -            "markers": "python_version >= '3.7'", -            "version": "==0.12.4" -        } -    } -} diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index afe3dd838..13de434bd 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -11,7 +11,8 @@          "ethernet": ["interfaces_ethernet"]      },      "interfaces_bridge": { -        "vxlan": ["interfaces_vxlan"] +        "vxlan": ["interfaces_vxlan"], +        "wlan": ["interfaces_wireless"]      },      "load_balancing_wan": {          "conntrack": ["system_conntrack"] diff --git a/data/configd-include.json b/data/configd-include.json index dc00f0698..dcee50306 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -81,7 +81,6 @@  "service_sla.py",  "service_ssh.py",  "service_tftp-server.py", -"service_upnp.py",  "service_webproxy.py",  "system_acceleration.py",  "system_config-management.py", @@ -108,6 +107,5 @@  "vpn_openconnect.py",  "vpn_pptp.py",  "vpn_sstp.py", -"vrf.py", -"vrf_vni.py" +"vrf.py"  ] diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index d3685caaf..c14133127 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -1,13 +1,16 @@  [  "accelppp.py",  "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/data/templates/accel-ppp/pppoe.config.j2 b/data/templates/accel-ppp/pppoe.config.j2 index ddf0da518..42bc8440c 100644 --- a/data/templates/accel-ppp/pppoe.config.j2 +++ b/data/templates/accel-ppp/pppoe.config.j2 @@ -67,7 +67,7 @@ service-name={{ service_name | join(',') }}  {%     set delay_without_sessions = pado_delay.delays_without_sessions[0] | default('0') %}  {%     set pado_delay_param = namespace(value=delay_without_sessions) %}  {%     for delay, sessions in pado_delay.delays_with_sessions | sort(attribute='1') %} -{%         if not loop.last %} +{%         if not delay == 'disable' %}  {%             set pado_delay_param.value = pado_delay_param.value + ',' + delay + ':' + sessions | string %}  {%         else %}  {%             set pado_delay_param.value = pado_delay_param.value + ',-1:' + sessions | string %} diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2 index b624f83a3..22fb55506 100644 --- a/data/templates/accel-ppp/sstp.config.j2 +++ b/data/templates/accel-ppp/sstp.config.j2 @@ -42,6 +42,9 @@ accept=ssl  ssl-ca-file=/run/accel-pppd/sstp-ca.pem  ssl-pemfile=/run/accel-pppd/sstp-cert.pem  ssl-keyfile=/run/accel-pppd/sstp-cert.key +{% if host_name is vyos_defined %} +host-name={{ host_name }} +{% endif %}  {% if default_pool is vyos_defined %}  ip-pool={{ default_pool }}  {% endif %} diff --git a/data/templates/firewall/upnpd.conf.j2 b/data/templates/firewall/upnpd.conf.j2 deleted file mode 100644 index 616e8869f..000000000 --- a/data/templates/firewall/upnpd.conf.j2 +++ /dev/null @@ -1,227 +0,0 @@ -# This is the UPNP configuration file - -# WAN network interface -ext_ifname={{ wan_interface }} -{% if wan_ip is vyos_defined %} - -# if the WAN network interface for IPv6 is different than for IPv4, -# set ext_ifname6 -#ext_ifname6=eth2 - -# If the WAN interface has several IP addresses, you -# can specify the one to use below. -# Setting ext_ip is also useful in double NAT setup, you can declare here -# the public IP address. -{%     for addr in wan_ip %} -ext_ip={{ addr }} -{%     endfor  %} -{% endif %} - -{% if stun is vyos_defined %} -# WAN interface must have public IP address. Otherwise it is behind NAT -# and port forwarding is impossible. In some cases WAN interface can be -# behind unrestricted full-cone NAT 1:1 when all incoming traffic is NAT-ed and -# routed to WAN interfaces without any filtering. In this cases miniupnpd -# needs to know public IP address and it can be learnt by asking external -# server via STUN protocol. Following option enable retrieving external -# public IP address from STUN server and detection of NAT type. You need -# to specify also external STUN server in stun_host option below. -# This option is disabled by default. -ext_perform_stun=yes -# Specify STUN server, either hostname or IP address -# Some public STUN servers: -#  stun.stunprotocol.org -#  stun.sipgate.net -#  stun.xten.com -#  stun.l.google.com (on non standard port 19302) -ext_stun_host={{ stun.host }} -# Specify STUN UDP port, by default it is standard port 3478. -ext_stun_port={{ stun.port }} -{% endif %} - -# LAN network interfaces IPs / networks -{% if listen is vyos_defined %} -# There can be multiple listening IPs for SSDP traffic, in that case -# use multiple 'listening_ip=...' lines, one for each network interface. -# It can be IP address or network interface name (ie. "eth0") -# It is mandatory to use the network interface name in order to enable IPv6 -# HTTP is available on all interfaces. -# When MULTIPLE_EXTERNAL_IP is enabled, the external IP -# address associated with the subnet follows. For example: -#  listening_ip=192.168.0.1/24 88.22.44.13 -# When MULTIPLE_EXTERNAL_IP is disabled, you can list associated network -# interfaces (for bridges) -#  listening_ip=bridge0 em0 wlan0 -{%     for addr in listen %} -{%         if addr | is_ipv4  %} -listening_ip={{ addr }} -{%         elif addr | is_ipv6  %} -ipv6_listening_ip={{ addr }} -{%         else %} -listening_ip={{ addr }} -{%         endif  %} -{%     endfor  %} -{% endif %} - -# CAUTION: mixing up WAN and LAN interfaces may introduce security risks! -# Be sure to assign the correct interfaces to LAN and WAN and consider -# implementing UPnP permission rules at the bottom of this configuration file - -# Port for HTTP (descriptions and SOAP) traffic. Set to 0 for autoselect. -#http_port=0 -# Port for HTTPS. Set to 0 for autoselect (default) -#https_port=0 - -# Path to the UNIX socket used to communicate with MiniSSDPd -# If running, MiniSSDPd will manage M-SEARCH answering. -# default is /var/run/minissdpd.sock -#minissdpdsocket=/var/run/minissdpd.sock - -{% if nat_pmp is vyos_defined %} -# Enable NAT-PMP support (default is no) -enable_natpmp=yes -{% endif %} - -# Enable UPNP support (default is yes) -enable_upnp=yes - -{% if pcp_lifetime is vyos_defined %} -# PCP -# Configure the minimum and maximum lifetime of a port mapping in seconds -# 120s and 86400s (24h) are suggested values from PCP-base -{%     if pcp_lifetime.max is vyos_defined %} -max_lifetime={{ pcp_lifetime.max }} -{%     endif %} -{%     if pcp_lifetime.min is vyos_defined %} -min_lifetime={{ pcp_lifetime.min }} -{%     endif %} -{% endif %} - -# table names for netfilter nft. Default is "filter" for both -#upnp_table_name= -#upnp_nat_table_name= -# chain names for netfilter and netfilter nft -# netfilter : default are MINIUPNPD, MINIUPNPD, MINIUPNPD-POSTROUTING -# netfilter nft : default are miniupnpd, prerouting_miniupnpd, postrouting_miniupnpd -#upnp_forward_chain=forwardUPnP -#upnp_nat_chain=UPnP -#upnp_nat_postrouting_chain=UPnP-Postrouting - -# Lease file location -lease_file=/config/upnp.leases - -# To enable the next few runtime options, see compile time -# ENABLE_MANUFACTURER_INFO_CONFIGURATION (config.h) - -{% if friendly_name is vyos_defined %} -# Name of this service, default is "`uname -s` router" -friendly_name={{ friendly_name }} -{% endif  %} - -# Manufacturer name, default is "`uname -s`" -manufacturer_name=VyOS - -# Manufacturer URL, default is URL of OS vendor -manufacturer_url=https://vyos.io/ - -# Model name, default is "`uname -s` router" -model_name=VyOS Router Model - -# Model description, default is "`uname -s` router" -model_description=Vyos open source enterprise router/firewall operating system - -# Model URL, default is URL of OS vendor -model_url=https://vyos.io/ - -# Bitrates reported by daemon in bits per second -# by default miniupnpd tries to get WAN interface speed -#bitrate_up=1000000 -#bitrate_down=10000000 - -{% if secure_mode is vyos_defined %} -# Secure Mode, UPnP clients can only add mappings to their own IP -secure_mode=yes -{% else %} -# Secure Mode, UPnP clients can only add mappings to their own IP -secure_mode=no -{% endif %} - -{% if presentation_url is vyos_defined %} -# Default presentation URL is HTTP address on port 80 -# If set to an empty string, no presentationURL element will appear -# in the XML description of the device, which prevents MS Windows -# from displaying an icon in the "Network Connections" panel. -#presentation_url= {{ presentation_url }} -{% endif %} - -# Report system uptime instead of daemon uptime -system_uptime=yes - -# Notify interval in seconds. default is 30 seconds. -#notify_interval=240 -notify_interval=60 - -# Unused rules cleaning. -# never remove any rule before this threshold for the number -# of redirections is exceeded. default to 20 -clean_ruleset_threshold=10 -# Clean process work interval in seconds. default to 0 (disabled). -# a 600 seconds (10 minutes) interval makes sense -clean_ruleset_interval=600 - -############################################################################ -## The next 5 config parameters (packet_log, anchor, queue, tag, quickrules) -## are specific to BSD's pf(4) packet filter and hence cannot be enabled in -## VyOS. -# Log packets in pf (default is no) -#packet_log=no - -# Anchor name in pf (default is miniupnpd) -#anchor=miniupnpd - -# ALTQ queue in pf -# Filter rules must be used for this to be used. -# compile with PF_ENABLE_FILTER_RULES (see config.h file) -#queue=queue_name1 - -# Tag name in pf -#tag=tag_name1 - -# Make filter rules in pf quick or not. default is yes -# active when compiled with PF_ENABLE_FILTER_RULES (see config.h file) -#quickrules=no -## -## End of pf(4)-specific configuration not to be set in VyOS. -############################################################################ - -# UUID, generate your own UUID with "make genuuid" -uuid={{ uuid }} - -# Daemon's serial and model number when reporting to clients -# (in XML description) -#serial=12345678 -#model_number=1 - -# If compiled with IGD_V2 defined, force reporting IGDv1 in rootDesc (default -# is no) -#force_igd_desc_v1=no - -{% if rule is vyos_defined %} -# UPnP permission rules (also enforced for NAT-PMP and PCP) -# (allow|deny) (external port range) IP/mask (internal port range) (optional regex filter) -# A port range is <min port>-<max port> or <port> if there is only -# one port in the range. -# IP/mask format must be nnn.nnn.nnn.nnn/nn -# It is advised to only allow redirection of port >= 1024 -# and end the rule set with "deny 0-65535 0.0.0.0/0 0-65535" -# The following default ruleset allows specific LAN side IP addresses -# to request only ephemeral ports. It is recommended that users -# modify the IP ranges to match their own internal networks, and -# also consider implementing network-specific restrictions -# CAUTION: failure to enforce any rules may permit insecure requests to be made! -{%     for rule, config in rule.items() %} -{%         if config.disable is not vyos_defined %} -{{ config.action }} {{ config.external_port_range }} {{ config.ip }}{{ '/32' if '/' not in config.ip else '' }} {{ config.internal_port_range }} -{%         endif %} -{%     endfor %} -{% endif %} diff --git a/data/templates/frr/zebra.vrf.route-map.frr.j2 b/data/templates/frr/zebra.vrf.route-map.frr.j2 index f1cc6fe66..8ebb82511 100644 --- a/data/templates/frr/zebra.vrf.route-map.frr.j2 +++ b/data/templates/frr/zebra.vrf.route-map.frr.j2 @@ -1,10 +1,6 @@  !  {% if name is vyos_defined %}  {%     for vrf, vrf_config in name.items() %} -{#         code path required for vrf_vni.py as we will only render the required VR configuration and not all of them #} -{%         if only_vrf is vyos_defined and vrf is not vyos_defined(only_vrf) %} -{%             continue %} -{%         endif %}  vrf {{ vrf }}  {%         if vrf_config.ip.nht.no_resolve_via_default is vyos_defined %}   no ip nht resolve-via-default @@ -25,7 +21,7 @@ vrf {{ vrf }}   ipv6 protocol {{ protocol_name }} route-map {{ protocol_config.route_map }}  {%             endfor %}  {%         endif %} -{%         if vrf_config.vni is vyos_defined and no_vni is not vyos_defined %} +{%         if vrf_config.vni is vyos_defined %}   vni {{ vrf_config.vni }}  {%         endif %}  exit-vrf diff --git a/data/templates/grub/grub_compat.j2 b/data/templates/grub/grub_compat.j2 index d1085eec8..8fb4f71dc 100644 --- a/data/templates/grub/grub_compat.j2 +++ b/data/templates/grub/grub_compat.j2 @@ -14,8 +14,6 @@      KVM  {%- elif type == 'ttyS' -%}      Serial -{%- elif type == 'ttyUSB' -%} -    USB  {%- else -%}      Unknown  {%- endif %} @@ -25,8 +23,6 @@      console=ttyS0,{{ console_speed }} console=tty0  {%- elif type == 'ttyS' -%}      console=tty0 console=ttyS0,{{ console_speed }} -{%- elif type == 'ttyUSB' -%} -    console=tty0 console=ttyUSB0,115200  {%- else -%}      console=tty0 console=ttyS0,{{ console_speed }}  {%- endif %} diff --git a/data/templates/grub/grub_options.j2 b/data/templates/grub/grub_options.j2 index c8a1472e1..a00bf4e37 100644 --- a/data/templates/grub/grub_options.j2 +++ b/data/templates/grub/grub_options.j2 @@ -33,12 +33,6 @@ submenu "Boot options" {              setup_serial              configfile ${prefix}/grub.cfg.d/*vyos-menu*.cfg          } -        menuentry "ttyUSB (USB serial)" { -            set console_type="ttyUSB" -            export console_type -            setup_serial -            configfile ${prefix}/grub.cfg.d/*vyos-menu*.cfg -        }      }      menuentry "Enter console number" {          read console_num diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2 index 7917c8257..797bf17e7 100644 --- a/data/templates/load-balancing/haproxy.cfg.j2 +++ b/data/templates/load-balancing/haproxy.cfg.j2 @@ -81,6 +81,11 @@ frontend {{ front }}  {%                     endif %}  {%                 endfor %}  {%             endif %} +{%             if front_config.http_response_headers is vyos_defined %} +{%                 for header, header_config in front_config.http_response_headers.items() %} +    http-response set-header {{ header }} '{{ header_config['value'] }}' +{%                 endfor %} +{%             endif %}  {%         endif %}  {%         if front_config.rule is vyos_defined %}  {%             for rule, rule_config in front_config.rule.items() %} @@ -158,6 +163,11 @@ backend {{ back }}  {%         endif %}  {%         if back_config.mode is vyos_defined %}      mode {{ back_config.mode }} +{%             if back_config.http_response_headers is vyos_defined %} +{%                 for header, header_config in back_config.http_response_headers.items() %} +    http-response set-header {{ header }} '{{ header_config['value'] }}' +{%                 endfor %} +{%             endif %}  {%         endif %}  {%         if back_config.rule is vyos_defined %}  {%             for rule, rule_config in back_config.rule.items() %} diff --git a/data/templates/wifi/hostapd.conf.j2 b/data/templates/wifi/hostapd.conf.j2 index 83009242b..769325b49 100644 --- a/data/templates/wifi/hostapd.conf.j2 +++ b/data/templates/wifi/hostapd.conf.j2 @@ -28,6 +28,12 @@ interface={{ ifname }}  {%     for bridge in is_bridge_member %}  bridge={{ bridge }}  {%     endfor %} + +# WDS (4-address frame) mode with per-station virtual interfaces +# (only supported with driver=nl80211) +# This mode allows associated stations to use 4-address frames to allow layer 2 +# bridging to be used. +wds_sta=1  {% endif %}  # Driver interface type (hostap/wired/none/nl80211/bsd); @@ -739,4 +745,3 @@ wmm_ac_vo_cwmin=2  wmm_ac_vo_cwmax=3  wmm_ac_vo_txop_limit=47  wmm_ac_vo_acm=0 - diff --git a/debian/control b/debian/control index 4151d83bb..2e99bdc28 100644 --- a/debian/control +++ b/debian/control @@ -10,6 +10,7 @@ Build-Depends:    iproute2,    libvyosconfig0 (>= 0.0.7),    libzmq3-dev, +  procps,    python3 (>= 3.10),  # For QA    pylint, @@ -197,9 +198,6 @@ Depends:    snmp,    snmpd,  # End "service snmp" -# For "service upnp" -  miniupnpd-nftables, -# End "service upnp"  # For "service webproxy"    squid,    squidclient, 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/firewall/match-interface.xml.i b/interface-definitions/include/firewall/match-interface.xml.i index 5da6f51fb..f25686e72 100644 --- a/interface-definitions/include/firewall/match-interface.xml.i +++ b/interface-definitions/include/firewall/match-interface.xml.i @@ -19,7 +19,7 @@        <description>Inverted interface name to match</description>      </valueHelp>      <constraint> -      <regex>(\!?)(bond|br|dum|en|ersp|eth|gnv|ifb|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)([0-9]?)(\*?)(.+)?|(\!?)lo</regex> +      <regex>(\!?)(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)([0-9]?)(\*?)(.+)?|(\!?)lo</regex>        <validator name="vrf-name"/>      </constraint>    </properties> diff --git a/interface-definitions/include/haproxy/http-response-headers.xml.i b/interface-definitions/include/haproxy/http-response-headers.xml.i new file mode 100644 index 000000000..9e7ddfd28 --- /dev/null +++ b/interface-definitions/include/haproxy/http-response-headers.xml.i @@ -0,0 +1,29 @@ +<!-- include start from haproxy/http-response-headers.xml.i --> +<tagNode name="http-response-headers"> +  <properties> +    <help>Headers to include in HTTP response</help> +    <valueHelp> +      <format>txt</format> +      <description>HTTP header name</description> +    </valueHelp> +    <constraint> +      <regex>[-a-zA-Z]+</regex> +    </constraint> +    <constraintErrorMessage>Header names must only include alphabetical characters and hyphens</constraintErrorMessage> +  </properties> +  <children> +    <leafNode name="value"> +      <properties> +        <help>HTTP header value</help> +        <valueHelp> +          <format>txt</format> +          <description>HTTP header value</description> +        </valueHelp> +        <constraint> +          <regex>[[:ascii:]]{1,256}</regex> +        </constraint> +      </properties> +    </leafNode> +  </children> +</tagNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/evpn-mh-uplink.xml.i b/interface-definitions/include/interface/evpn-mh-uplink.xml.i new file mode 100644 index 000000000..5f7fe1b7f --- /dev/null +++ b/interface-definitions/include/interface/evpn-mh-uplink.xml.i @@ -0,0 +1,8 @@ +<!-- include start from interface/evpn-mh-uplink.xml.i --> +<leafNode name="uplink"> +  <properties> +    <help>Uplink to the VXLAN core</help> +    <valueless/> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/nat-translation-options.xml.i b/interface-definitions/include/nat-translation-options.xml.i index 6b95de045..c8900590f 100644 --- a/interface-definitions/include/nat-translation-options.xml.i +++ b/interface-definitions/include/nat-translation-options.xml.i @@ -28,22 +28,18 @@        <properties>          <help>Port mapping options</help>          <completionHelp> -          <list>random fully-random none</list> +          <list>random none</list>          </completionHelp>          <valueHelp>            <format>random</format>            <description>Randomize source port mapping</description>          </valueHelp>          <valueHelp> -          <format>fully-random</format> -          <description>Full port randomization</description> -        </valueHelp> -        <valueHelp>            <format>none</format>            <description>Do not apply port randomization</description>          </valueHelp>          <constraint> -          <regex>(random|fully-random|none)</regex> +          <regex>(random|none)</regex>          </constraint>        </properties>        <defaultValue>none</defaultValue> 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/include/version/nat-version.xml.i b/interface-definitions/include/version/nat-version.xml.i index 656da6e14..173e91ed3 100644 --- a/interface-definitions/include/version/nat-version.xml.i +++ b/interface-definitions/include/version/nat-version.xml.i @@ -1,3 +1,3 @@  <!-- include start from include/version/nat-version.xml.i --> -<syntaxVersion component='nat' version='7'></syntaxVersion> +<syntaxVersion component='nat' version='8'></syntaxVersion>  <!-- include end --> diff --git a/interface-definitions/include/version/pppoe-server-version.xml.i b/interface-definitions/include/version/pppoe-server-version.xml.i index c253c58d9..61de1277a 100644 --- a/interface-definitions/include/version/pppoe-server-version.xml.i +++ b/interface-definitions/include/version/pppoe-server-version.xml.i @@ -1,3 +1,3 @@  <!-- include start from include/version/pppoe-server-version.xml.i --> -<syntaxVersion component='pppoe-server' version='9'></syntaxVersion> +<syntaxVersion component='pppoe-server' version='10'></syntaxVersion>  <!-- include end --> diff --git a/interface-definitions/interfaces_bonding.xml.in b/interface-definitions/interfaces_bonding.xml.in index 92c0911db..cc0327f3d 100644 --- a/interface-definitions/interfaces_bonding.xml.in +++ b/interface-definitions/interfaces_bonding.xml.in @@ -102,12 +102,7 @@                    </constraint>                  </properties>                </leafNode> -              <leafNode name="uplink"> -                <properties> -                  <help>Uplink to the VXLAN core</help> -                  <valueless/> -                </properties> -              </leafNode> +              #include <include/interface/evpn-mh-uplink.xml.i>              </children>            </node>            <leafNode name="hash-policy"> @@ -176,6 +171,18 @@              </properties>              <defaultValue>0</defaultValue>            </leafNode> +          <leafNode name="system-mac"> +            <properties> +              <help>System MAC address for 802.3ad</help> +              <valueHelp> +                <format>macaddr</format> +                <description>MAC address</description> +              </valueHelp> +              <constraint> +                <validator name="mac-address"/> +              </constraint> +            </properties> +          </leafNode>            <leafNode name="lacp-rate">              <properties>                <help>Rate in which we will ask our link partner to transmit LACPDU packets</help> diff --git a/interface-definitions/interfaces_ethernet.xml.in b/interface-definitions/interfaces_ethernet.xml.in index 4e55bac7c..89f990d41 100644 --- a/interface-definitions/interfaces_ethernet.xml.in +++ b/interface-definitions/interfaces_ethernet.xml.in @@ -57,6 +57,14 @@              <defaultValue>auto</defaultValue>            </leafNode>            #include <include/interface/eapol.xml.i> +          <node name="evpn"> +            <properties> +              <help>EVPN Multihoming</help> +            </properties> +            <children> +              #include <include/interface/evpn-mh-uplink.xml.i> +            </children> +          </node>            #include <include/interface/hw-id.xml.i>            #include <include/interface/ipv4-options.xml.i>            #include <include/interface/ipv6-options.xml.i> diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in index 7b46f32b3..23cc83e9a 100644 --- a/interface-definitions/interfaces_openvpn.xml.in +++ b/interface-definitions/interfaces_openvpn.xml.in @@ -663,7 +663,7 @@                            <help>Number of digits to use for totp hash</help>                            <valueHelp>                              <format>1-65535</format> -                            <description>Seconds</description> +                            <description>Digits</description>                            </valueHelp>                            <constraint>                              <validator name="numeric" argument="--range 1-65535"/> diff --git a/interface-definitions/load-balancing_reverse-proxy.xml.in b/interface-definitions/load-balancing_reverse-proxy.xml.in index 6a3b3cef1..011e1b53c 100644 --- a/interface-definitions/load-balancing_reverse-proxy.xml.in +++ b/interface-definitions/load-balancing_reverse-proxy.xml.in @@ -39,6 +39,7 @@                #include <include/port-number.xml.i>                #include <include/haproxy/rule-frontend.xml.i>                #include <include/haproxy/tcp-request.xml.i> +              #include <include/haproxy/http-response-headers.xml.i>                <leafNode name="redirect-http-to-https">                  <properties>                    <help>Redirect HTTP to HTTPS</help> @@ -90,6 +91,7 @@                </leafNode>                #include <include/generic-description.xml.i>                #include <include/haproxy/mode.xml.i> +              #include <include/haproxy/http-response-headers.xml.i>                <node name="parameters">                  <properties>                    <help>Backend parameters</help> diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index 0a639bd80..73a748137 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -141,6 +141,7 @@                  </children>                </node>                #include <include/inbound-interface.xml.i> +              #include <include/firewall/log.xml.i>                <node name="translation">                  <properties>                    <help>Translation address or prefix</help> 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/policy.xml.in b/interface-definitions/policy.xml.in index 791fa1d87..eb907cb9e 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -1546,11 +1546,11 @@                      <properties>                        <help>Set prefixes to table</help>                        <valueHelp> -                        <format>u32:1-200</format> +                        <format>u32:1-4294967295</format>                          <description>Table value</description>                        </valueHelp>                        <constraint> -                        <validator name="numeric" argument="--range 1-200"/> +                        <validator name="numeric" argument="--range 1-4294967295"/>                        </constraint>                      </properties>                    </leafNode> diff --git a/interface-definitions/service_config-sync.xml.in b/interface-definitions/service_config-sync.xml.in index e9ea9aa4b..648c14aee 100644 --- a/interface-definitions/service_config-sync.xml.in +++ b/interface-definitions/service_config-sync.xml.in @@ -34,6 +34,10 @@                    </constraint>                  </properties>                </leafNode> +              #include <include/port-number.xml.i> +              <leafNode name="port"> +                <defaultValue>443</defaultValue> +              </leafNode>                <leafNode name="timeout">                  <properties>                    <help>Connection API timeout</help> diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in index 9b5e4d3fb..5d357c2f9 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service_pppoe-server.xml.in @@ -74,11 +74,19 @@              <properties>                <help>PADO delays</help>                <valueHelp> +                <format>disable</format> +                <description>Disable new connections</description> +              </valueHelp> +              <completionHelp> +                <list>disable</list> +              </completionHelp> +              <valueHelp>                  <format>u32:1-999999</format>                  <description>Number in ms</description>                </valueHelp>                <constraint>                  <validator name="numeric" argument="--range 1-999999"/> +                <regex>disable</regex>                </constraint>                <constraintErrorMessage>Invalid PADO delay</constraintErrorMessage>              </properties> diff --git a/interface-definitions/service_upnp.xml.in b/interface-definitions/service_upnp.xml.in deleted file mode 100644 index 064386ee5..000000000 --- a/interface-definitions/service_upnp.xml.in +++ /dev/null @@ -1,229 +0,0 @@ -<?xml version="1.0"?> -<interfaceDefinition> -  <node name="service"> -    <children> -      <node name="upnp" owner="${vyos_conf_scripts_dir}/service_upnp.py"> -        <properties> -          <help>Universal Plug and Play (UPnP) service</help> -          <priority>900</priority> -        </properties> -        <children> -          <leafNode name="friendly-name"> -            <properties> -              <help>Name of this service</help> -              <valueHelp> -                <format>txt</format> -                <description>Friendly name</description> -              </valueHelp> -            </properties> -          </leafNode> -          <leafNode name="wan-interface"> -            <properties> -              <help>WAN network interface</help> -              <completionHelp> -                <script>${vyos_completion_dir}/list_interfaces</script> -              </completionHelp> -              <constraint> -                #include <include/constraint/interface-name.xml.i> -              </constraint> -            </properties> -          </leafNode> -          <leafNode name="wan-ip"> -            <properties> -              <help>WAN network IP</help> -              <valueHelp> -                <format>ipv4</format> -                <description>IPv4 address</description> -              </valueHelp> -              <valueHelp> -                <format>ipv6</format> -                <description>IPv6 address</description> -              </valueHelp> -              <constraint> -                <validator name="ipv4-address" /> -                <validator name="ipv6-address" /> -              </constraint> -              <multi/> -            </properties> -          </leafNode> -          <leafNode name="nat-pmp"> -            <properties> -              <help>Enable NAT-PMP support</help> -              <valueless /> -            </properties> -          </leafNode> -          <leafNode name="secure-mode"> -            <properties> -              <help>Enable Secure Mode</help> -              <valueless /> -            </properties> -          </leafNode> -          <leafNode name="presentation-url"> -            <properties> -              <help>Presentation Url</help> -              <valueHelp> -                <format>txt</format> -                <description>Presentation Url</description> -              </valueHelp> -            </properties> -          </leafNode> -          <node name="pcp-lifetime"> -            <properties> -              <help>PCP-base lifetime Option</help> -            </properties> -            <children> -              <leafNode name="max"> -                <properties> -                  <help>Max lifetime time</help> -                  <constraint> -                    <validator name="numeric" /> -                  </constraint> -                </properties> -              </leafNode> -              <leafNode name="min"> -                <properties> -                  <help>Min lifetime time</help> -                  <constraint> -                    <validator name="numeric" /> -                  </constraint> -                </properties> -              </leafNode> -            </children> -          </node> -          <leafNode name="listen"> -            <properties> -              <help>Local IP addresses for service to listen on</help> -              <completionHelp> -                <script>${vyos_completion_dir}/list_local_ips.sh --both</script> -                <script>${vyos_completion_dir}/list_interfaces</script> -              </completionHelp> -              <valueHelp> -                <format><interface></format> -                <description>Monitor interface address</description> -              </valueHelp> -              <valueHelp> -                <format>ipv4</format> -                <description>IPv4 address to listen for incoming connections</description> -              </valueHelp> -              <valueHelp> -                <format>ipv4net</format> -                <description>IPv4 prefix to listen for incoming connections</description> -              </valueHelp> -              <valueHelp> -                <format>ipv6</format> -                <description>IPv6 address to listen for incoming connections</description> -              </valueHelp> -              <valueHelp> -                <format>ipv6net</format> -                <description>IPv6 prefix to listen for incoming connections</description> -              </valueHelp> -              <multi/> -              <constraint> -                #include <include/constraint/interface-name.xml.i> -                <validator name="ip-address"/> -                <validator name="ipv4-prefix"/> -                <validator name="ipv6-prefix"/> -              </constraint> -            </properties> -          </leafNode> -          <node name="stun"> -            <properties> -              <help>Enable STUN probe support (can be used with NAT 1:1 support for WAN interfaces)</help> -            </properties> -            <children> -              <leafNode name="host"> -                <properties> -                  <help>The STUN server address</help> -                  <valueHelp> -                    <format>txt</format> -                    <description>The STUN server host address</description> -                  </valueHelp> -                  <constraint> -                    <validator name="fqdn"/> -                  </constraint> -                </properties> -              </leafNode> -              #include <include/port-number.xml.i> -            </children> -          </node> -          <tagNode name="rule"> -            <properties> -              <help>UPnP Rule</help> -              <valueHelp> -                <format>u32:0-65535</format> -                <description>Rule number</description> -              </valueHelp> -              <constraint> -                <validator name="numeric" argument="--range 0-65535"/> -              </constraint> -            </properties> -            <children> -              #include <include/generic-disable-node.xml.i> -              <leafNode name="external-port-range"> -                <properties> -                  <help>Port range (REQUIRE)</help> -                  <valueHelp> -                    <format><port></format> -                    <description>single port</description> -                  </valueHelp> -                  <valueHelp> -                    <format><portN>-<portM></format> -                    <description>Port range (use '-' as delimiter)</description> -                  </valueHelp> -                  <constraint> -                    <validator name="port-range"/> -                  </constraint> -                </properties> -              </leafNode> -              <leafNode name="internal-port-range"> -                <properties> -                  <help>Port range (REQUIRE)</help> -                  <valueHelp> -                    <format><port></format> -                    <description>single port</description> -                  </valueHelp> -                  <valueHelp> -                    <format><portN>-<portM></format> -                    <description>Port range (use '-' as delimiter)</description> -                  </valueHelp> -                  <constraint> -                    <validator name="port-range"/> -                  </constraint> -                </properties> -              </leafNode> -              <leafNode name="ip"> -                <properties> -                  <help>The IP to which this rule applies (REQUIRE)</help> -                  <valueHelp> -                    <format>ipv4</format> -                    <description>The IPv4 address to which this rule applies</description> -                  </valueHelp> -                  <valueHelp> -                    <format>ipv4net</format> -                    <description>The IPv4 to which this rule applies</description> -                  </valueHelp> -                  <constraint> -                    <validator name="ipv4-address"/> -                    <validator name="ipv4-host"/> -                    <validator name="ipv4-prefix"/> -                  </constraint> -                </properties> -              </leafNode> -              <leafNode name="action"> -                <properties> -                  <help>Actions against the rule (REQUIRE)</help> -                  <completionHelp> -                    <list>allow deny</list> -                  </completionHelp> -                  <constraint> -                    <regex>(allow|deny)</regex> -                  </constraint> -                </properties> -              </leafNode> -            </children> -          </tagNode> -        </children> -      </node> -    </children> -  </node> -</interfaceDefinition> 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/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index d23a001d5..d9ed1c040 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -53,6 +53,15 @@            #include <include/accel-ppp/wins-server.xml.i>            #include <include/generic-description.xml.i>            #include <include/name-server-ipv4-ipv6.xml.i> +          <leafNode name="host-name"> +            <properties> +              <help>Only allow connection to specified host with the same TLS SNI</help> +              <constraint> +                #include <include/constraint/host-name.xml.i> +              </constraint> +              <constraintErrorMessage>Host-name must be alphanumeric and can contain hyphens</constraintErrorMessage> +            </properties> +          </leafNode>          </children>        </node>      </children> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index 94ed96e4b..a20be995a 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -120,20 +120,7 @@                <constraintErrorMessage>VRF routing table must be in range from 100 to 65535</constraintErrorMessage>              </properties>            </leafNode> -          <leafNode name="vni" owner="${vyos_conf_scripts_dir}/vrf_vni.py $VAR(../@)"> -            <properties> -              <help>Virtual Network Identifier</help> -              <!-- must be after BGP to keep correct order when removing L3VNIs in FRR --> -              <priority>822</priority> -              <valueHelp> -                <format>u32:0-16777214</format> -                <description>VXLAN virtual network identifier</description> -              </valueHelp> -              <constraint> -                <validator name="numeric" argument="--range 0-16777214"/> -              </constraint> -            </properties> -          </leafNode> +          #include <include/vni.xml.i>          </children>        </tagNode>      </children> diff --git a/op-mode-definitions/force-commit-archive.xml.in b/op-mode-definitions/force-commit-archive.xml.in index 162323c20..46836f967 100644 --- a/op-mode-definitions/force-commit-archive.xml.in +++ b/op-mode-definitions/force-commit-archive.xml.in @@ -6,7 +6,7 @@          <properties>            <help>Manually archive configuration</help>          </properties> -        <command>/usr/bin/config-mgmt</command> +        <command>/etc/commit/post-hooks.d/02vyos-commit-archive; printf "\n"</command>        </leafNode>      </children>    </node> 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..13e7fd81d 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -7,6 +7,39 @@            <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> +                <children> +                  <tagNode name="external-address"> +                    <properties> +                      <help>Show CGNAT allocations for an external IP address</help> +                      <completionHelp> +                        <list><x.x.x.x></list> +                      </completionHelp> +                    </properties> +                    <command>sudo ${vyos_op_scripts_dir}/cgnat.py show_allocation --external-address "$6"</command> +                  </tagNode> +                  <tagNode name="internal-address"> +                    <properties> +                      <help>Show CGNAT allocations for an internal IP address</help> +                      <completionHelp> +                        <list><x.x.x.x></list> +                      </completionHelp> +                    </properties> +                    <command>sudo ${vyos_op_scripts_dir}/cgnat.py show_allocation --internal-address "$6"</command> +                  </tagNode> +                </children> +                <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/ntp.xml.in b/op-mode-definitions/ntp.xml.in index b8d0c43ec..17250a45e 100644 --- a/op-mode-definitions/ntp.xml.in +++ b/op-mode-definitions/ntp.xml.in @@ -6,13 +6,25 @@          <properties>            <help>Show peer status of NTP daemon</help>          </properties> -        <command>${vyos_op_scripts_dir}/show_ntp.sh --sourcestats</command> +        <command>${vyos_op_scripts_dir}/ntp.py show_sourcestats</command>          <children> +          <node name="activity"> +            <properties> +              <help>Report the number of servers and peers that are online and offline</help> +            </properties> +            <command>${vyos_op_scripts_dir}/ntp.py show_activity</command> +          </node> +          <node name="sources"> +            <properties> +              <help>Show information about the current time sources being accessed</help> +            </properties> +            <command>${vyos_op_scripts_dir}/ntp.py show_sources</command> +          </node>            <node name="system">              <properties>                <help>Show parameters about the system clock performance</help>              </properties> -            <command>${vyos_op_scripts_dir}/show_ntp.sh --tracking</command> +            <command>${vyos_op_scripts_dir}/ntp.py show_tracking</command>            </node>          </children>        </node> 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/op-mode-definitions/show-interfaces-bonding.xml.in b/op-mode-definitions/show-interfaces-bonding.xml.in index 8ca5adb4f..e2950331b 100644 --- a/op-mode-definitions/show-interfaces-bonding.xml.in +++ b/op-mode-definitions/show-interfaces-bonding.xml.in @@ -23,8 +23,27 @@                  <properties>                    <help>Show detailed interface information</help>                  </properties> -                <command>if [ -f "/proc/net/bonding/$4" ]; then cat "/proc/net/bonding/$4"; else echo "Interface $4 does not exist!"; fi</command> +                <command>if [ -f "/proc/net/bonding/$4" ]; then sudo cat "/proc/net/bonding/$4"; else echo "Interface $4 does not exist!"; fi</command>                </leafNode> +              <node name="lacp"> +                <properties> +                  <help>Show LACP related info</help> +                </properties> +                <children> +                  <leafNode name="detail"> +                    <properties> +                      <help>Show LACP details</help> +                    </properties> +                    <command>sudo ${vyos_op_scripts_dir}/bonding.py show_lacp_detail --interface="$4" </command> +                  </leafNode> +                  <leafNode name="neighbors"> +                    <properties> +                      <help>Show LACP Neighbors</help> +                    </properties> +                    <command>sudo ${vyos_op_scripts_dir}/bonding.py show_lacp_neighbors --interface="$4"</command> +                  </leafNode> +                </children> +              </node>                <leafNode name="slaves">                  <properties>                    <help>Show specified bonding interface information</help> @@ -62,6 +81,19 @@                  </properties>                  <command>${vyos_op_scripts_dir}/interfaces.py show --intf-type=bonding</command>                </leafNode> +              <node name="lacp"> +                <properties> +                  <help>Show LACP related info</help> +                </properties> +                <children> +                  <leafNode name="detail"> +                    <properties> +                      <help>Show LACP details</help> +                    </properties> +                    <command>sudo ${vyos_op_scripts_dir}/bonding.py show_lacp_detail</command> +                  </leafNode> +                </children> +              </node>                <leafNode name="slaves">                  <properties>                    <help>Show specified bonding interface information</help> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index e13270364..c3aa324ba 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -464,12 +464,56 @@              </properties>              <command>journalctl --no-hostname --boot --unit lldpd.service</command>            </leafNode> -          <leafNode name="nat"> +          <node name="nat">              <properties>                <help>Show log for Network Address Translation (NAT)</help>              </properties> -            <command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command> -          </leafNode> +            <children> +              <node name="destination"> +                <properties> +                  <help>Show NAT destination log</help> +                </properties> +                <command>journalctl --no-hostname --boot -k | egrep "\[DST-NAT-[0-9]+\]"</command> +                <children> +                  <tagNode name="rule"> +                    <properties> +                      <help>Show NAT destination log for specified rule</help> +                    </properties> +                    <command>journalctl --no-hostname --boot -k | egrep "\[DST-NAT-$6\]"</command> +                  </tagNode> +                </children> +              </node> +              <node name="source"> +                <properties> +                  <help>Show NAT source log</help> +                </properties> +                <command>journalctl --no-hostname --boot -k | egrep "\[SRC-NAT-[0-9]+(-MASQ)?\]"""</command> +                <children> +                  <tagNode name="rule"> +                    <properties> +                      <help>Show NAT source log for specified rule</help> +                    </properties> +                    <command>journalctl --no-hostname --boot -k | egrep "\[SRC-NAT-$6(-MASQ)?\]"</command> +                  </tagNode> +                </children> +              </node> +              <node name="static"> +                <properties> +                  <help>Show NAT static log</help> +                </properties> +                <command>journalctl --no-hostname --boot -k | egrep "\[STATIC-(SRC|DST)-NAT-[0-9]+\]"</command> +                <children> +                  <tagNode name="rule"> +                    <properties> +                      <help>Show NAT static log for specified rule</help> +                    </properties> +                    <command>journalctl --no-hostname --boot -k | egrep "\[STATIC-(SRC|DST)-NAT-$6\]"</command> +                  </tagNode> +                </children> +              </node> +            </children> +            <command>journalctl --no-hostname --boot -k | egrep "\[(STATIC-)?(DST|SRC)-NAT-[0-9]+(-MASQ)?\]"</command> +          </node>            <leafNode name="ndp-proxy">              <properties>                <help>Show log for Neighbor Discovery Protocol (NDP) Proxy</help> diff --git a/op-mode-definitions/system-image.xml.in b/op-mode-definitions/system-image.xml.in index 7b5260b4e..44b055be6 100644 --- a/op-mode-definitions/system-image.xml.in +++ b/op-mode-definitions/system-image.xml.in @@ -72,6 +72,15 @@            <help>Set system operational parameters</help>          </properties>          <children> +          <tagNode name="boot-console"> +            <properties> +              <help>Set system console type at boot</help> +              <completionHelp> +                <script>sudo ${vyos_op_scripts_dir}/image_manager.py --action list_console_types</script> +              </completionHelp> +            </properties> +            <command>sudo ${vyos_op_scripts_dir}/image_manager.py --action set_console_type --console-type "${4}"</command> +          </tagNode>            <node name="image">              <properties>                <help>Set system image parameters</help> diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index fc51d781c..70b6ea203 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -283,6 +283,8 @@ Proceed ?'''          rollback_ct = self._get_config_tree_revision(rev)          try:              load(rollback_ct, switch='explicit') +            print('Rollback diff has been applied.') +            print('Use "compare" to review the changes or "commit" to apply them.')          except LoadConfigError as e:              raise ConfigMgmtError(e) from e diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index d9d605a9d..d7b7b80a8 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -32,7 +32,6 @@ from vyos.utils.process import cmd  from vyos.utils.process import run  # Conntrack -  def conntrack_required(conf):      required_nodes = ['nat', 'nat66', 'load-balancing wan'] @@ -454,8 +453,28 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):                  else:                      output.append(f'set update ip{def_suffix} saddr @DA{def_suffix}_{dyn_group}') +    set_table = False      if 'set' in rule_conf: -        output.append(parse_policy_set(rule_conf['set'], def_suffix)) +        # Parse set command used in policy route: +        if 'connection_mark' in rule_conf['set']: +            conn_mark = rule_conf['set']['connection_mark'] +            output.append(f'ct mark set {conn_mark}') +        if 'dscp' in rule_conf['set']: +            dscp = rule_conf['set']['dscp'] +            output.append(f'ip{def_suffix} dscp set {dscp}') +        if 'mark' in rule_conf['set']: +            mark = rule_conf['set']['mark'] +            output.append(f'meta mark set {mark}') +        if 'table' in rule_conf['set']: +            set_table = True +            table = rule_conf['set']['table'] +            if table == 'main': +                table = '254' +            mark = 0x7FFFFFFF - int(table) +            output.append(f'meta mark set {mark}') +        if 'tcp_mss' in rule_conf['set']: +            mss = rule_conf['set']['tcp_mss'] +            output.append(f'tcp option maxseg size set {mss}')      if 'action' in rule_conf:          # Change action=return to action=action @@ -488,6 +507,10 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):              if synproxy_ws:                  output.append(f'wscale {synproxy_ws} timestamp sack-perm') +    else: +        if set_table: +            output.append('return') +      output.append(f'comment "{family}-{hook}-{fw_name}-{rule_id}"')      return " ".join(output) @@ -518,28 +541,6 @@ def parse_time(time):          out.append(f'day {{{",".join(out_days)}}}')      return " ".join(out) -def parse_policy_set(set_conf, def_suffix): -    out = [] -    if 'connection_mark' in set_conf: -        conn_mark = set_conf['connection_mark'] -        out.append(f'ct mark set {conn_mark}') -    if 'dscp' in set_conf: -        dscp = set_conf['dscp'] -        out.append(f'ip{def_suffix} dscp set {dscp}') -    if 'mark' in set_conf: -        mark = set_conf['mark'] -        out.append(f'meta mark set {mark}') -    if 'table' in set_conf: -        table = set_conf['table'] -        if table == 'main': -            table = '254' -        mark = 0x7FFFFFFF - int(table) -        out.append(f'meta mark set {mark}') -    if 'tcp_mss' in set_conf: -        mss = set_conf['tcp_mss'] -        out.append(f'tcp option maxseg size set {mss}') -    return " ".join(out) -  # GeoIP  nftables_geoip_conf = '/run/nftables-geoip.conf' diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index c6d0f1cff..b8ea90049 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -18,6 +18,7 @@ import os  from vyos.ifconfig.interface import Interface  from vyos.utils.dict import dict_search  from vyos.utils.assertion import assert_list +from vyos.utils.assertion import assert_mac  from vyos.utils.assertion import assert_positive  @Interface.register @@ -54,6 +55,10 @@ class BondIf(Interface):              'validate': lambda v: assert_list(v, ['slow', 'fast']),              'location': '/sys/class/net/{ifname}/bonding/lacp_rate',          }, +        'bond_system_mac': { +            'validate': lambda v: assert_mac(v, test_all_zero=False), +            'location': '/sys/class/net/{ifname}/bonding/ad_actor_system', +        },          'bond_miimon': {              'validate': assert_positive,              'location': '/sys/class/net/{ifname}/bonding/miimon' @@ -385,6 +390,24 @@ class BondIf(Interface):          """          return self.set_interface('bond_mode', mode) +    def set_system_mac(self, mac): +        """ +        In an AD system, this specifies the mac-address for the actor in +	    protocol packet exchanges (LACPDUs). The value cannot be NULL or +	    multicast. It is preferred to have the local-admin bit set for this +	    mac but driver does not enforce it. If the value is not given then +	    system defaults to using the masters' mac address as actors' system +	    address. + +	    This parameter has effect only in 802.3ad mode and is available through +	    SysFs interface. + +        Example: +        >>> from vyos.ifconfig import BondIf +        >>> BondIf('bond0').set_system_mac('00:50:ab:cd:ef:01') +        """ +        return self.set_interface('bond_system_mac', mac) +      def update(self, config):          """ General helper function which works on a dictionary retrived by          get_config_dict(). It's main intention is to consolidate the scattered @@ -426,14 +449,13 @@ class BondIf(Interface):                      Interface(interface).set_admin_state('up')              # Bonding policy/mode - default value, always present -            mode = config.get('mode') -            self.set_mode(mode) +            self.set_mode(config['mode'])              # LACPDU transmission rate - default value -            if mode == '802.3ad': +            if config['mode'] == '802.3ad':                  self.set_lacp_rate(config.get('lacp_rate')) -            if mode not in ['802.3ad', 'balance-tlb', 'balance-alb']: +            if config['mode'] not in ['802.3ad', 'balance-tlb', 'balance-alb']:                  tmp = dict_search('arp_monitor.interval', config)                  value = tmp if (tmp != None) else '0'                  self.set_arp_interval(value) @@ -468,6 +490,14 @@ class BondIf(Interface):                  Interface(interface).flush_addrs()                  self.add_port(interface) +        # Add system mac address for 802.3ad - default address is all zero +        # mode is always present (defaultValue) +        if config['mode'] == '802.3ad': +            mac = '00:00:00:00:00:00' +            if 'system_mac' in config: +                mac = config['system_mac'] +            self.set_system_mac(mac) +          # Primary device interface - must be set after 'mode'          value = config.get('primary')          if value: self.set_primary(value) diff --git a/python/vyos/nat.py b/python/vyos/nat.py index 2ada29add..e54548788 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -300,12 +300,12 @@ def parse_nat_static_rule(rule_conf, rule_id, nat_type):      output.append('counter') -    if translation_str: -        output.append(translation_str) -      if 'log' in rule_conf:          output.append(f'log prefix "[{log_prefix}{log_suffix}]"') +    if translation_str: +        output.append(translation_str) +      output.append(f'comment "{log_prefix}"')      return " ".join(output) diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py index 4173a1a43..98e486e42 100644 --- a/python/vyos/qos/base.py +++ b/python/vyos/qos/base.py @@ -90,13 +90,14 @@ class QoSBase:          else:              return value -    def _calc_random_detect_queue_params(self, avg_pkt, max_thr, limit=None, min_thr=None, mark_probability=None): +    def _calc_random_detect_queue_params(self, avg_pkt, max_thr, limit=None, min_thr=None, +                                         mark_probability=None, precedence=0):          params = dict()          avg_pkt = int(avg_pkt)          max_thr = int(max_thr)          mark_probability = int(mark_probability)          limit = int(limit) if limit else 4 * max_thr -        min_thr = int(min_thr) if min_thr else (9 * max_thr) // 18 +        min_thr = int(min_thr) if min_thr else ((9 + precedence) * max_thr) // 18          params['avg_pkt'] = avg_pkt          params['limit'] = limit * avg_pkt @@ -246,9 +247,15 @@ class QoSBase:                  filter_cmd_base += ' protocol all'                  if 'match' in cls_config: -                    is_filtered = False +                    has_filter = False                      for index, (match, match_config) in enumerate(cls_config['match'].items(), start=1):                          filter_cmd = filter_cmd_base +                        if not has_filter: +                            for key in ['mark', 'vif', 'ip', 'ipv6']: +                                if key in match_config: +                                    has_filter = True +                                    break +                          if self.qostype == 'shaper' and 'prio ' not in filter_cmd:                              filter_cmd += f' prio {index}'                          if 'mark' in match_config: @@ -331,13 +338,12 @@ class QoSBase:                                  cls = int(cls)                                  filter_cmd += f' flowid {self._parent:x}:{cls:x}'                                  self._cmd(filter_cmd) -                                is_filtered = True                      vlan_expression = "match.*.vif"                      match_vlan = jmespath.search(vlan_expression, cls_config)                      if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config) \ -                        and is_filtered: +                        and has_filter:                          # For "vif" "basic match" is used instead of "action police" T5961                          if not match_vlan:                              filter_cmd += f' action police' diff --git a/python/vyos/qos/randomdetect.py b/python/vyos/qos/randomdetect.py index d7d84260f..a3a39da36 100644 --- a/python/vyos/qos/randomdetect.py +++ b/python/vyos/qos/randomdetect.py @@ -21,33 +21,25 @@ class RandomDetect(QoSBase):      # https://man7.org/linux/man-pages/man8/tc.8.html      def update(self, config, direction): -        tmp = f'tc qdisc add dev {self._interface} root handle {self._parent}:0 dsmark indices 8 set_tc_index' +        # # Generalized Random Early Detection +        handle = self._parent +        tmp = f'tc qdisc add dev {self._interface} root handle {self._parent}:0 gred setup DPs 8 default 0 grio'          self._cmd(tmp) - -        tmp = f'tc filter add dev {self._interface} parent {self._parent}:0 protocol ip prio 1 tcindex mask 0xe0 shift 5' -        self._cmd(tmp) - -        # Generalized Random Early Detection -        handle = self._parent +1 -        tmp = f'tc qdisc add dev {self._interface} parent {self._parent}:0 handle {handle}:0 gred setup DPs 8 default 0 grio' -        self._cmd(tmp) -          bandwidth = self._rate_convert(config['bandwidth'])          # set VQ (virtual queue) parameters          for precedence, precedence_config in config['precedence'].items():              precedence = int(precedence) -            avg_pkt = int(precedence_config['average_packet']) -            limit = int(precedence_config['queue_limit']) * avg_pkt -            min_val = int(precedence_config['minimum_threshold']) * avg_pkt -            max_val = int(precedence_config['maximum_threshold']) * avg_pkt - -            tmp  = f'tc qdisc change dev {self._interface} handle {handle}:0 gred limit {limit} min {min_val} max {max_val} avpkt {avg_pkt} ' - -            burst = (2 * int(precedence_config['minimum_threshold']) + int(precedence_config['maximum_threshold'])) // 3 -            probability = 1 / int(precedence_config['mark_probability']) -            tmp += f'burst {burst} bandwidth {bandwidth} probability {probability} DP {precedence} prio {8 - precedence:x}' - +            qparams = self._calc_random_detect_queue_params( +                avg_pkt=precedence_config.get('average_packet'), +                max_thr=precedence_config.get('maximum_threshold'), +                limit=precedence_config.get('queue_limit'), +                min_thr=precedence_config.get('minimum_threshold'), +                mark_probability=precedence_config.get('mark_probability'), +                precedence=precedence +            ) +            tmp = f'tc qdisc change dev {self._interface} handle {handle}:0 gred limit {qparams["limit"]} min {qparams["min_val"]} max {qparams["max_val"]} avpkt {qparams["avg_pkt"]} ' +            tmp += f'burst {qparams["burst"]} bandwidth {bandwidth} probability {qparams["probability"]} DP {precedence} prio {8 - precedence:x}'              self._cmd(tmp)          # call base class diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py index 1b487c1d2..d35bddea2 100644 --- a/python/vyos/system/compat.py +++ b/python/vyos/system/compat.py @@ -220,14 +220,8 @@ def get_default(data: dict, root_dir: str = '') -> Union[int, None]:      sublist = list(filter(lambda x: (x.get('version') == image_name and                                       x.get('console_type') == console_type and -                                     x.get('console_num') == console_num and                                       x.get('bootmode') == 'normal'),                            menu_entries)) -    # legacy images added with legacy tools omitted 'ttyUSB'; if entry not -    # available, default to initial entry of version -    if not sublist: -        sublist = list(filter(lambda x: x.get('version') == image_name, -                              menu_entries))      if sublist:          return menu_entries.index(sublist[0]) @@ -253,6 +247,10 @@ def update_version_list(root_dir: str = '') -> list[dict]:      menu_entries = parse_menuentries(grub_cfg_main)      menu_versions = find_versions(menu_entries) +    # remove deprecated console-type ttyUSB +    menu_entries = list(filter(lambda x: x.get('console_type') != 'ttyUSB', +                               menu_entries)) +      # get list of versions added/removed by image-tools      current_versions = grub.version_list(root_dir) 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/template.py b/python/vyos/template.py index ac77e8a3d..fbc5f1456 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -25,6 +25,14 @@ from vyos.utils.file import makedir  from vyos.utils.permission import chmod  from vyos.utils.permission import chown +# We use a mutable global variable for the default template directory +# to make it possible to call scripts from this repository +# outside of live VyOS systems. +# If something (like the image build scripts) +# want to call a script, they can modify the default location +# to the repository path. +DEFAULT_TEMPLATE_DIR = directories["templates"] +  # Holds template filters registered via register_filter()  _FILTERS = {}  _TESTS = {} @@ -35,18 +43,7 @@ def _get_environment(location=None):      from os import getenv      if location is None: -        # Sometimes functions that rely on templates need to be executed outside of VyOS installations: -        # for example, installer functions are executed for image builds, -        # and anything may be invoked for testing from a developer's machine. -        # This environment variable allows running any unmodified code -        # with a custom template location. -        location_env_var = getenv("VYOS_TEMPLATE_DIR") -        if location_env_var: -            print(f"Using environment variable {location_env_var}") -            template_dir = location_env_var -        else: -            template_dir = directories["templates"] -        loc_loader=FileSystemLoader(template_dir) +        loc_loader=FileSystemLoader(DEFAULT_TEMPLATE_DIR)      else:          loc_loader=FileSystemLoader(location)      env = Environment( diff --git a/python/vyos/tpm.py b/python/vyos/tpm.py index b9f28546f..a24f149fd 100644 --- a/python/vyos/tpm.py +++ b/python/vyos/tpm.py @@ -15,7 +15,7 @@  import os  import tempfile -from vyos.util import rc_cmd +from vyos.utils.process import rc_cmd  default_pcrs = ['0','2','4','7']  tpm_handle = 0x81000000 diff --git a/python/vyos/utils/assertion.py b/python/vyos/utils/assertion.py index 1aaa54dff..c7fa220c3 100644 --- a/python/vyos/utils/assertion.py +++ b/python/vyos/utils/assertion.py @@ -53,7 +53,7 @@ def assert_mtu(mtu, ifname):      if (max_mtu and cur_mtu > max_mtu) or cur_mtu > 65536:          raise ValueError(f'MTU is too small for interface "{ifname}": {mtu} > {max_mtu}') -def assert_mac(m): +def assert_mac(m, test_all_zero=True):      split = m.split(':')      size = len(split) @@ -74,7 +74,7 @@ def assert_mac(m):          raise ValueError(f'{m} is a multicast MAC address')      # overall mac address is not allowed to be 00:00:00:00:00:00 -    if sum(octets) == 0: +    if test_all_zero and sum(octets) == 0:          raise ValueError('00:00:00:00:00:00 is not a valid MAC address')      if octets[:5] == (0, 0, 94, 0, 1): diff --git a/python/vyos/utils/io.py b/python/vyos/utils/io.py index a8c430f28..205210b66 100644 --- a/python/vyos/utils/io.py +++ b/python/vyos/utils/io.py @@ -72,6 +72,8 @@ def ask_yes_no(question, default=False) -> bool:                  stdout.write("Please respond with yes/y or no/n\n")          except EOFError:              stdout.write("\nPlease respond with yes/y or no/n\n") +        except KeyboardInterrupt: +            return False  def is_interactive():      """Try to determine if the routine was called from an interactive shell.""" 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/python/vyos/xml_ref/__init__.py b/python/vyos/xml_ref/__init__.py index bf434865d..2ba3da4e8 100644 --- a/python/vyos/xml_ref/__init__.py +++ b/python/vyos/xml_ref/__init__.py @@ -53,6 +53,12 @@ def is_valueless(path: list) -> bool:  def is_leaf(path: list) -> bool:      return load_reference().is_leaf(path) +def owner(path: list) -> str: +    return load_reference().owner(path) + +def priority(path: list) -> str: +    return load_reference().priority(path) +  def cli_defined(path: list, node: str, non_local=False) -> bool:      return load_reference().cli_defined(path, node, non_local=non_local) diff --git a/python/vyos/xml_ref/definition.py b/python/vyos/xml_ref/definition.py index c90c5ddbc..c85835ffd 100644 --- a/python/vyos/xml_ref/definition.py +++ b/python/vyos/xml_ref/definition.py @@ -135,6 +135,33 @@ class Xml:          d = self._get_ref_path(path)          return self._is_leaf_node(d) +    def _least_upper_data(self, path: list, name: str) -> str: +        ref_path = path.copy() +        d = self.ref +        data = '' +        while ref_path and d: +            d = d.get(ref_path[0], {}) +            ref_path.pop(0) +            if self._is_tag_node(d) and ref_path: +                ref_path.pop(0) +            if self._is_leaf_node(d) and ref_path: +                ref_path.pop(0) +            res = self._get_ref_node_data(d, name) +            if res is not None: +                data = res + +        return data + +    def owner(self, path: list) -> str: +        from pathlib import Path +        data = self._least_upper_data(path, 'owner') +        if data: +            data = Path(data.split()[0]).name +        return data + +    def priority(self, path: list) -> str: +        return self._least_upper_data(path, 'priority') +      @staticmethod      def _dict_get(d: dict, path: list) -> dict:          for i in path: 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/config-tests/nat-basic b/smoketest/config-tests/nat-basic new file mode 100644 index 000000000..9fea08b02 --- /dev/null +++ b/smoketest/config-tests/nat-basic @@ -0,0 +1,85 @@ +set interfaces ethernet eth0 offload rps +set interfaces ethernet eth0 disable +set interfaces ethernet eth1 offload gro +set interfaces ethernet eth1 offload gso +set interfaces ethernet eth1 offload rps +set interfaces ethernet eth1 offload sg +set interfaces ethernet eth1 offload tso +set interfaces ethernet eth2 offload gro +set interfaces ethernet eth2 offload gso +set interfaces ethernet eth2 offload rps +set interfaces ethernet eth2 offload sg +set interfaces ethernet eth2 offload tso +set interfaces ethernet eth3 offload gro +set interfaces ethernet eth3 offload gso +set interfaces ethernet eth3 offload rps +set interfaces ethernet eth3 offload sg +set interfaces ethernet eth3 offload tso +set interfaces bonding bond10 hash-policy 'layer3+4' +set interfaces bonding bond10 member interface 'eth2' +set interfaces bonding bond10 member interface 'eth3' +set interfaces bonding bond10 mode '802.3ad' +set interfaces bonding bond10 vif 50 address '192.168.189.1/24' +set interfaces loopback lo +set interfaces pppoe pppoe7 authentication password 'vyos' +set interfaces pppoe pppoe7 authentication username 'vyos' +set interfaces pppoe pppoe7 dhcpv6-options pd 0 interface bond10.50 address '1' +set interfaces pppoe pppoe7 dhcpv6-options pd 0 length '56' +set interfaces pppoe pppoe7 ip adjust-mss '1452' +set interfaces pppoe pppoe7 ipv6 address autoconf +set interfaces pppoe pppoe7 ipv6 adjust-mss '1432' +set interfaces pppoe pppoe7 mtu '1492' +set interfaces pppoe pppoe7 no-peer-dns +set interfaces pppoe pppoe7 source-interface 'eth1' +set service lldp interface eth1 disable +set service ntp allow-client address '192.168.189.0/24' +set service ntp server time1.vyos.net +set service ntp server time2.vyos.net +set service ntp listen-address '192.168.189.1' +set service ssh dynamic-protection +set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 lease '604800' +set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 option default-router '192.168.189.1' +set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 option domain-name 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 option name-server '1.1.1.1' +set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 option name-server '9.9.9.9' +set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 range 0 start '192.168.189.20' +set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 range 0 stop '192.168.189.254' +set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 subnet-id '1' +set service router-advert interface bond10.50 prefix ::/64 preferred-lifetime '2700' +set service router-advert interface bond10.50 prefix ::/64 valid-lifetime '5400' +set system config-management commit-revisions '100' +set system domain-name 'vyos.net' +set system host-name 'R1' +set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' +set system login user vyos authentication plaintext-password '' +set system name-server '1.1.1.1' +set system name-server '9.9.9.9' +set system console device ttyS0 speed '115200' +set nat destination rule 1000 destination port '3389' +set nat destination rule 1000 inbound-interface name 'pppoe7' +set nat destination rule 1000 protocol 'tcp' +set nat destination rule 1000 translation address '192.168.189.5' +set nat destination rule 1000 translation port '3389' +set nat destination rule 10022 destination port '10022' +set nat destination rule 10022 inbound-interface name 'pppoe7' +set nat destination rule 10022 protocol 'tcp' +set nat destination rule 10022 translation address '192.168.189.2' +set nat destination rule 10022 translation port '22' +set nat destination rule 10300 destination port '10300' +set nat destination rule 10300 inbound-interface name 'pppoe7' +set nat destination rule 10300 protocol 'udp' +set nat destination rule 10300 translation address '192.168.189.2' +set nat destination rule 10300 translation port '10300' +set nat source rule 10 outbound-interface name 'eth1' +set nat source rule 10 source address '192.168.189.0/24' +set nat source rule 10 translation address 'masquerade' +set nat source rule 10 translation options port-mapping 'random' +set nat source rule 50 outbound-interface name 'pppoe7' +set nat source rule 50 protocol 'udp' +set nat source rule 50 source address '192.168.189.2' +set nat source rule 50 source port '10300' +set nat source rule 50 translation address 'masquerade' +set nat source rule 50 translation port '10300' +set nat source rule 100 outbound-interface name 'pppoe7' +set nat source rule 100 source address '192.168.189.0/24' +set nat source rule 100 translation address 'masquerade' 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/configs/nat-basic b/smoketest/configs/nat-basic new file mode 100644 index 000000000..52f369f34 --- /dev/null +++ b/smoketest/configs/nat-basic @@ -0,0 +1,256 @@ +interfaces { +    bonding bond10 { +        hash-policy "layer3+4" +        member { +            interface "eth2" +            interface "eth3" +        } +        mode "802.3ad" +        vif 50 { +            address "192.168.189.1/24" +        } +    } +    ethernet eth0 { +        disable +        offload { +            gro +            gso +            rps +            sg +            tso +        } +    } +    ethernet eth1 { +        offload { +            gro +            gso +            rps +            sg +            tso +        } +    } +    ethernet eth2 { +        offload { +            gro +            gso +            rps +            sg +            tso +        } +    } +    ethernet eth3 { +        offload { +            gro +            gso +            rps +            sg +            tso +        } +    } +    loopback lo { +    } +    pppoe pppoe7 { +        authentication { +            password "vyos" +            username "vyos" +        } +        dhcpv6-options { +            pd 0 { +                interface bond10.50 { +                    address "1" +                } +                length "56" +            } +        } +        ip { +            adjust-mss "1452" +        } +        ipv6 { +            address { +                autoconf +            } +            adjust-mss "1432" +        } +        mtu "1492" +        no-peer-dns +        source-interface "eth1" +    } +} +nat { +    destination { +        rule 1000 { +            destination { +                port "3389" +            } +            inbound-interface { +                name "pppoe7" +            } +            protocol "tcp" +            translation { +                address "192.168.189.5" +                port "3389" +            } +        } +        rule 10022 { +            destination { +                port "10022" +            } +            inbound-interface { +                name "pppoe7" +            } +            protocol "tcp" +            translation { +                address "192.168.189.2" +                port "22" +            } +        } +        rule 10300 { +            destination { +                port "10300" +            } +            inbound-interface { +                name "pppoe7" +            } +            protocol "udp" +            translation { +                address "192.168.189.2" +                port "10300" +            } +        } +    } +    source { +        rule 10 { +            outbound-interface { +                name "eth1" +            } +            source { +                address "192.168.189.0/24" +            } +            translation { +                address "masquerade" +                options { +                    port-mapping fully-random +                } +            } +        } +        rule 50 { +            outbound-interface { +                name "pppoe7" +            } +            protocol "udp" +            source { +                address "192.168.189.2" +                port "10300" +            } +            translation { +                address "masquerade" +                port "10300" +            } +        } +        rule 100 { +            outbound-interface { +                name "pppoe7" +            } +            source { +                address "192.168.189.0/24" +            } +            translation { +                address "masquerade" +            } +        } +    } +} +service { +    dhcp-server { +        shared-network-name LAN { +            subnet 192.168.189.0/24 { +                default-router "192.168.189.1" +                domain-name "vyos.net" +                lease "604800" +                name-server "1.1.1.1" +                name-server "9.9.9.9" +                range 0 { +                    start "192.168.189.20" +                    stop "192.168.189.254" +                } +            } +        } +    } +    lldp { +        interface all { +        } +        interface eth1 { +            disable +        } +    } +    ntp { +        allow-client { +            address "192.168.189.0/24" +        } +        listen-address "192.168.189.1" +        server time1.vyos.net { +        } +        server time2.vyos.net { +        } +    } +    router-advert { +        interface bond10.50 { +            prefix ::/64 { +                preferred-lifetime "2700" +                valid-lifetime "5400" +            } +        } +    } +    ssh { +        disable-host-validation +        dynamic-protection { +        } +    } +} +system { +    config-management { +        commit-revisions "100" +    } +    conntrack { +        modules { +            ftp +            h323 +            nfs +            pptp +            sip +            sqlnet +            tftp +        } +    } +    console { +        device ttyS0 { +            speed "115200" +        } +    } +    domain-name "vyos.net" +    host-name "R1" +    login { +        user vyos { +            authentication { +                encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ +                plaintext-password "" +            } +        } +    } +    name-server "1.1.1.1" +    name-server "9.9.9.9" +    syslog { +        global { +            facility all { +                level "info" +            } +            facility local7 { +                level "debug" +            } +        } +    } +} + +// Warning: Do not remove the following line. +// vyos-config-version: "bgp@5:broadcast-relay@1:cluster@2:config-management@1:conntrack@5:conntrack-sync@2:container@2:dhcp-relay@2:dhcp-server@8:dhcpv6-server@1:dns-dynamic@4:dns-forwarding@4:firewall@15:flow-accounting@1:https@6:ids@1:interfaces@32:ipoe-server@3:ipsec@13:isis@3:l2tp@9:lldp@2:mdns@1:monitoring@1:nat@7:nat66@3:ntp@3:openconnect@3:ospf@2:pim@1:policy@8:pppoe-server@10:pptp@5:qos@2:quagga@11:rip@1:rpki@2:salt@1:snmp@3:ssh@2:sstp@6:system@27:vrf@3:vrrp@4:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2" +// Release version: 1.4.0-epa3 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_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 419de774a..f436424b8 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -241,6 +241,34 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):              for member in self._members:                  self.assertIn(member, slaves) +    def test_bonding_system_mac(self): +        # configure member interfaces and system-mac +        default_system_mac = '00:00:00:00:00:00' # default MAC is all zeroes +        system_mac = '00:50:ab:cd:ef:11' + +        for interface in self._interfaces: +            for option in self._options.get(interface, []): +                self.cli_set(self._base_path + [interface] + option.split()) + +            self.cli_set(self._base_path + [interface, 'system-mac', system_mac]) + +        self.cli_commit() + +        # verify config +        for interface in self._interfaces: +            tmp = read_file(f'/sys/class/net/{interface}/bonding/ad_actor_system') +            self.assertIn(tmp, system_mac) + +        for interface in self._interfaces: +            self.cli_delete(self._base_path + [interface, 'system-mac']) + +        self.cli_commit() + +        # verify default value +        for interface in self._interfaces: +            tmp = read_file(f'/sys/class/net/{interface}/bonding/ad_actor_system') +            self.assertIn(tmp, default_system_mac) +      def test_bonding_evpn_multihoming(self):          id = '5'          for interface in self._interfaces: diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index 8f387b23d..4843a40da 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -354,5 +354,15 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):                  out = loads(out)                  self.assertFalse(out[0]['autonegotiate']) +    def test_ethtool_evpn_uplink_tarcking(self): +        for interface in self._interfaces: +            self.cli_set(self._base_path + [interface, 'evpn', 'uplink']) + +        self.cli_commit() + +        for interface in self._interfaces: +            frrconfig = self.getFRRconfig(f'interface {interface}', daemon='zebra') +            self.assertIn(f' evpn mh uplink', frrconfig) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index 83b00ac0c..b45754cae 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -236,6 +236,17 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):          self.assertIn(interface, bridge_members) +        # Now generate a VLAN on the bridge +        self.cli_set(bridge_path + ['enable-vlan']) +        self.cli_set(bridge_path + ['vif', '20', 'address', '10.0.0.1/24']) + +        self.cli_commit() + +        tmp = get_config_value(interface, 'bridge') +        self.assertEqual(tmp, bridge) +        tmp = get_config_value(interface, 'wds_sta') +        self.assertEqual(tmp, '1') +          self.cli_delete(bridge_path)      def test_wireless_security_station_address(self): diff --git a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py index c8b17316f..370a9276a 100755 --- a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py +++ b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py @@ -385,5 +385,26 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):          self.assertIn(f'mode {mode}', config)          self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config) +    def test_07_lb_reverse_proxy_http_response_headers(self): +        # Setup base +        self.configure_pki() +        self.base_config() + +        # Set example headers in both frontend and backend +        self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800']) +        self.cli_set(base_path + ['backend', 'bk-01',  'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01']) +        self.cli_commit() + +        # Test headers are present in generated configuration file +        config = read_file(HAPROXY_CONF) +        self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config) +        self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config) + +        # Test setting alongside modes other than http is blocked by validation conditions +        self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp']) +        with self.assertRaises(ConfigSessionError) as e: +            self.cli_commit() + +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index ee4445251..a0c6ab055 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1923,6 +1923,66 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):          self.assertEqual(sort_ip(tmp), sort_ip(original_second)) +    def test_frr_individual_remove_T6283_T6250(self): +        path = base_path + ['route-map'] +        route_maps = ['RMAP-1', 'RMAP_2'] +        seq = '10' +        base_local_preference = 300 +        base_table = 50 + +        # T6250 +        local_preference = base_local_preference +        table = base_table +        for route_map in route_maps: +            self.cli_set(path + [route_map, 'rule', seq, 'action', 'permit']) +            self.cli_set(path + [route_map, 'rule', seq, 'set', 'table', str(table)]) +            self.cli_set(path + [route_map, 'rule', seq, 'set', 'local-preference', str(local_preference)]) +            local_preference += 20 +            table += 5 + +        self.cli_commit() + +        local_preference = base_local_preference +        table = base_table +        for route_map in route_maps: +            config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') +            self.assertIn(f' set local-preference {local_preference}', config) +            self.assertIn(f' set table {table}', config) +            local_preference += 20 +            table += 5 + +        for route_map in route_maps: +            self.cli_delete(path + [route_map, 'rule', '10', 'set', 'table']) +            # we explicitly commit multiple times to be as vandal as possible to the system +            self.cli_commit() + +        local_preference = base_local_preference +        for route_map in route_maps: +            config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') +            self.assertIn(f' set local-preference {local_preference}', config) +            local_preference += 20 + +        # T6283 +        seq = '20' +        prepend = '100 100 100' +        for route_map in route_maps: +            self.cli_set(path + [route_map, 'rule', seq, 'action', 'permit']) +            self.cli_set(path + [route_map, 'rule', seq, 'set', 'as-path', 'prepend', prepend]) + +        self.cli_commit() + +        for route_map in route_maps: +            config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') +            self.assertIn(f' set as-path prepend {prepend}', config) + +        for route_map in route_maps: +            self.cli_delete(path + [route_map, 'rule', seq, 'set']) +            # we explicitly commit multiple times to be as vandal as possible to the system +            self.cli_commit() + +        for route_map in route_maps: +            config = self.getFRRconfig(f'route-map {route_map} permit {seq}', end='') +            self.assertNotIn(f' set', config)  def sort_ip(output):      o = '\n'.join([' '.join(line.strip().split()) for line in output.strip().splitlines()]) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 03daa34aa..ea2f561a4 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -1330,6 +1330,27 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):          self.assertIn(f'neighbor {ext_neighbors[1]} remote-as external', conf)          self.assertIn(f'neighbor {ext_neighbors[1]} peer-group {ext_pg_name}', conf) +    def test_bgp_29_peer_group_remote_as_equal_local_as(self): +        self.cli_set(base_path + ['system-as', ASN]) +        self.cli_set(base_path + ['peer-group', 'OVERLAY', 'local-as', f'{int(ASN) + 1}']) +        self.cli_set(base_path + ['peer-group', 'OVERLAY', 'remote-as', f'{int(ASN) + 1}']) +        self.cli_set(base_path + ['peer-group', 'OVERLAY', 'address-family', 'l2vpn-evpn']) + +        self.cli_set(base_path + ['peer-group', 'UNDERLAY', 'address-family', 'ipv4-unicast']) + +        self.cli_set(base_path + ['neighbor', '10.177.70.62', 'peer-group', 'UNDERLAY']) +        self.cli_set(base_path + ['neighbor', '10.177.70.62', 'remote-as', 'external']) + +        self.cli_set(base_path + ['neighbor', '10.177.75.1', 'peer-group', 'OVERLAY']) +        self.cli_set(base_path + ['neighbor', '10.177.75.2', 'peer-group', 'OVERLAY']) + +        self.cli_commit() + +        conf = self.getFRRconfig(f'router bgp {ASN}') + +        self.assertIn(f'neighbor OVERLAY remote-as {int(ASN) + 1}', conf) +        self.assertIn(f'neighbor OVERLAY local-as {int(ASN) + 1}', conf) +      def test_bgp_99_bmp(self):          target_name = 'instance-bmp'          target_address = '127.0.0.1' 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_qos.py b/smoketest/scripts/cli/test_qos.py index fef1ff23a..5977b2f41 100755 --- a/smoketest/scripts/cli/test_qos.py +++ b/smoketest/scripts/cli/test_qos.py @@ -441,7 +441,6 @@ class TestQoS(VyOSUnitTestSHIM.TestCase):          self.cli_commit()      def test_08_random_detect(self): -        self.skipTest('tc returns invalid JSON here - needs iproute2 fix')          bandwidth = 5000          first = True @@ -467,8 +466,11 @@ class TestQoS(VyOSUnitTestSHIM.TestCase):          bandwidth = 5000          for interface in self._interfaces:              tmp = get_tc_qdisc_json(interface) -            import pprint -            pprint.pprint(tmp) +            self.assertTrue('gred' in tmp.get('kind')) +            self.assertEqual(8, len(tmp.get('options', {}).get('vqs'))) +            self.assertEqual(8, tmp.get('options', {}).get('dp_cnt')) +            self.assertEqual(0, tmp.get('options', {}).get('dp_default')) +            self.assertTrue(tmp.get('options', {}).get('grio'))      def test_09_rate_control(self):          bandwidth = 5000 @@ -736,6 +738,27 @@ class TestQoS(VyOSUnitTestSHIM.TestCase):          self.cli_commit()          self.assertEqual('', cmd(f'tc filter show dev {interface}')) +    def test_14_policy_limiter_marked_traffic(self): +        policy_name = 'smoke_test' +        base_policy_path = ['qos', 'policy', 'limiter', policy_name] + +        self.cli_set(['qos', 'interface', self._interfaces[0], 'ingress', policy_name]) +        self.cli_set(base_policy_path + ['class', '100', 'bandwidth', '20gbit']) +        self.cli_set(base_policy_path + ['class', '100', 'burst', '3760k']) +        self.cli_set(base_policy_path + ['class', '100', 'match', 'INTERNAL', 'mark', '100']) +        self.cli_set(base_policy_path + ['class', '100', 'priority', '20']) +        self.cli_set(base_policy_path + ['default', 'bandwidth', '1gbit']) +        self.cli_set(base_policy_path + ['default', 'burst', '125000000b']) +        self.cli_commit() + +        tc_filters = cmd(f'tc filter show dev {self._interfaces[0]} ingress') +        # class 100 +        self.assertIn('filter parent ffff: protocol all pref 20 fw chain 0', tc_filters) +        self.assertIn('action order 1:  police 0x1 rate 20Gbit burst 3847500b mtu 2Kb action drop overhead 0b', tc_filters) +        # default +        self.assertIn('filter parent ffff: protocol all pref 255 basic chain 0', tc_filters) +        self.assertIn('action order 1:  police 0x2 rate 1Gbit burst 125000000b mtu 2Kb action drop overhead 0b', tc_filters) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 5a48b1f58..97c63d4cb 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -168,7 +168,14 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase):          conf = ConfigParser(allow_no_value=True, delimiters='=')          conf.read(self._config_file) -        self.assertEqual(conf['pppoe']['pado-delay'], '10,20:200,-1:300') +        self.assertEqual(conf['pppoe']['pado-delay'], '10,20:200,30:300') + +        self.set(['pado-delay', 'disable', 'sessions', '400']) +        self.cli_commit() + +        conf = ConfigParser(allow_no_value=True, delimiters='=') +        conf.read(self._config_file) +        self.assertEqual(conf['pppoe']['pado-delay'], '10,20:200,30:300,-1:400')  if __name__ == '__main__': diff --git a/smoketest/scripts/cli/test_service_upnp.py b/smoketest/scripts/cli/test_service_upnp.py deleted file mode 100755 index fd67b0ced..000000000 --- a/smoketest/scripts/cli/test_service_upnp.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021-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.template import ip_from_cidr -from vyos.utils.file import read_file -from vyos.utils.process import process_named_running - -UPNP_CONF = '/run/upnp/miniupnp.conf' -DAEMON = 'miniupnpd' -interface = 'eth0' -base_path = ['service', 'upnp'] -address_base = ['interfaces', 'ethernet', interface, 'address'] - -ipv4_addr = '100.64.0.1/24' -ipv6_addr = '2001:db8::1/64' - -class TestServiceUPnP(VyOSUnitTestSHIM.TestCase): -    @classmethod -    def setUpClass(cls): -        super(TestServiceUPnP, 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) - -        cls.cli_set(cls, address_base + [ipv4_addr]) -        cls.cli_set(cls, address_base + [ipv6_addr]) - -    @classmethod -    def tearDownClass(cls): -        cls.cli_delete(cls, address_base) -        cls._session.commit() - -        super(TestServiceUPnP, cls).tearDownClass() - -    def tearDown(self): -        # Check for running process -        self.assertTrue(process_named_running(DAEMON)) - -        self.cli_delete(base_path) -        self.cli_commit() - -        # Check for running process -        self.assertFalse(process_named_running(DAEMON)) - -    def test_ipv4_base(self): -        self.cli_set(base_path + ['nat-pmp']) -        self.cli_set(base_path + ['listen', interface]) - -        # check validate() - WAN interface is mandatory -        with self.assertRaises(ConfigSessionError): -            self.cli_commit() -        self.cli_set(base_path + ['wan-interface', interface]) - -        self.cli_commit() - -        config = read_file(UPNP_CONF) -        self.assertIn(f'ext_ifname={interface}', config) -        self.assertIn(f'listening_ip={interface}', config) -        self.assertIn(f'enable_natpmp=yes', config) -        self.assertIn(f'enable_upnp=yes', config) - -    def test_ipv6_base(self): -        v6_addr = ip_from_cidr(ipv6_addr) - -        self.cli_set(base_path + ['nat-pmp']) -        self.cli_set(base_path + ['listen', interface]) -        self.cli_set(base_path + ['listen', v6_addr]) - -        # check validate() - WAN interface is mandatory -        with self.assertRaises(ConfigSessionError): -            self.cli_commit() -        self.cli_set(base_path + ['wan-interface', interface]) - -        self.cli_commit() - -        config = read_file(UPNP_CONF) -        self.assertIn(f'ext_ifname={interface}', config) -        self.assertIn(f'listening_ip={interface}', config) -        self.assertIn(f'ipv6_listening_ip={v6_addr}', config) -        self.assertIn(f'enable_natpmp=yes', config) -        self.assertIn(f'enable_upnp=yes', config) - -if __name__ == '__main__': -    unittest.main(verbosity=2) 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/smoketest/scripts/cli/test_vpn_sstp.py b/smoketest/scripts/cli/test_vpn_sstp.py index f0695d577..1a3e1df6e 100755 --- a/smoketest/scripts/cli/test_vpn_sstp.py +++ b/smoketest/scripts/cli/test_vpn_sstp.py @@ -75,6 +75,16 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase):          config = read_file(self._config_file)          self.assertIn(f'port={port}', config) +    def test_sstp_host_name(self): +        host_name = 'test.vyos.io' +        self.set(['host-name', host_name]) + +        self.basic_config() +        self.cli_commit() + +        config = read_file(self._config_file) +        self.assertIn(f'host-name={host_name}', config) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index f6e4181c0..243397dc2 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -18,7 +18,6 @@ import re  import os  import unittest -from netifaces import interfaces  from base_vyostest_shim import VyOSUnitTestSHIM  from vyos.configsession import ConfigSessionError @@ -27,6 +26,7 @@ from vyos.ifconfig import Section  from vyos.utils.file import read_file  from vyos.utils.network import get_interface_config  from vyos.utils.network import is_intf_addr_assigned +from vyos.utils.network import interface_exists  from vyos.utils.system import sysctl_read  base_path = ['vrf'] @@ -60,7 +60,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          self.cli_delete(base_path)          self.cli_commit()          for vrf in vrfs: -            self.assertNotIn(vrf, interfaces()) +            self.assertFalse(interface_exists(vrf))      def test_vrf_vni_and_table_id(self):          base_table = '1000' @@ -89,7 +89,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          iproute2_config = read_file('/etc/iproute2/rt_tables.d/vyos-vrf.conf')          for vrf in vrfs:              description = f'VyOS-VRF-{vrf}' -            self.assertTrue(vrf in interfaces()) +            self.assertTrue(interface_exists(vrf))              vrf_if = Interface(vrf)              # validate proper interface description              self.assertEqual(vrf_if.get_alias(), description) @@ -131,7 +131,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          loopbacks = ['127.0.0.1', '::1']          for vrf in vrfs:              # Ensure VRF was created -            self.assertIn(vrf, interfaces()) +            self.assertTrue(interface_exists(vrf))              # Verify IP forwarding is 1 (enabled)              self.assertEqual(sysctl_read(f'net.ipv4.conf.{vrf}.forwarding'), '1')              self.assertEqual(sysctl_read(f'net.ipv6.conf.{vrf}.forwarding'), '1') @@ -171,7 +171,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          self.cli_commit()          # Check if VRF has been created -        self.assertTrue(vrf in interfaces()) +        self.assertTrue(interface_exists(vrf))          table = str(int(table) + 1)          self.cli_set(base + ['table', table]) @@ -228,7 +228,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):              next_hop = f'192.0.{table}.1'              prefix = f'10.0.{table}.0/24' -            self.assertTrue(vrf in interfaces()) +            self.assertTrue(interface_exists(vrf))              frrconfig = self.getFRRconfig(f'vrf {vrf}')              self.assertIn(f' vni {table}', frrconfig) @@ -261,7 +261,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          # Apply VRF config          self.cli_commit()          # Ensure VRF got created -        self.assertIn(vrf, interfaces()) +        self.assertTrue(interface_exists(vrf))          # ... and IP addresses are still assigned          for address in addresses:              self.assertTrue(is_intf_addr_assigned(interface, address)) @@ -293,7 +293,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          loopbacks = ['127.0.0.1', '::1']          for vrf in vrfs:              # Ensure VRF was created -            self.assertIn(vrf, interfaces()) +            self.assertTrue(interface_exists(vrf))              # Verify IP forwarding is 0 (disabled)              self.assertEqual(sysctl_read(f'net.ipv4.conf.{vrf}.forwarding'), '0')              self.assertEqual(sysctl_read(f'net.ipv6.conf.{vrf}.forwarding'), '0') @@ -425,7 +425,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          # Verify VRF configuration          table = base_table          for vrf in vrfs: -            self.assertTrue(vrf in interfaces()) +            self.assertTrue(interface_exists(vrf))              frrconfig = self.getFRRconfig(f'vrf {vrf}')              self.assertIn(f' vni {table}', frrconfig) @@ -447,7 +447,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          # Verify VRF configuration          table = base_table          for vrf in vrfs: -            self.assertTrue(vrf in interfaces()) +            self.assertTrue(interface_exists(vrf))              frrconfig = self.getFRRconfig(f'vrf {vrf}')              self.assertIn(f' vni {table}', frrconfig) @@ -470,13 +470,39 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          # Verify VRF configuration          table = base_table          for vrf in vrfs: -            self.assertTrue(vrf in interfaces()) +            self.assertTrue(interface_exists(vrf))              frrconfig = self.getFRRconfig(f'vrf {vrf}')              self.assertIn(f' vni {table}', frrconfig)              # Increment table ID for the next run              table = str(int(table) + 2) + +        # add a new VRF with VNI - this must not delete any existing VRF/VNI +        purple = 'purple' +        table = str(int(table) + 10) +        self.cli_set(base_path + ['name', purple, 'table', table]) +        self.cli_set(base_path + ['name', purple, 'vni', table]) + +        # commit changes +        self.cli_commit() + +        # Verify VRF configuration +        table = base_table +        for vrf in vrfs: +            self.assertTrue(interface_exists(vrf)) + +            frrconfig = self.getFRRconfig(f'vrf {vrf}') +            self.assertIn(f' vni {table}', frrconfig) +            # Increment table ID for the next run +            table = str(int(table) + 2) + +        # Verify purple VRF/VNI +        self.assertTrue(interface_exists(purple)) +        table = str(int(table) + 10) +        frrconfig = self.getFRRconfig(f'vrf {purple}') +        self.assertIn(f' vni {table}', frrconfig) +          # Now delete all the VNIs          for vrf in vrfs:              base = base_path + ['name', vrf] @@ -487,11 +513,16 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):          # Verify no VNI is defined          for vrf in vrfs: -            self.assertTrue(vrf in interfaces()) +            self.assertTrue(interface_exists(vrf))              frrconfig = self.getFRRconfig(f'vrf {vrf}')              self.assertNotIn('vni', frrconfig) +        # Verify purple VNI remains +        self.assertTrue(interface_exists(purple)) +        frrconfig = self.getFRRconfig(f'vrf {purple}') +        self.assertIn(f' vni {table}', frrconfig) +      def test_vrf_ip_ipv6_nht(self):          table = '6910' diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py index 0e3cbd0ed..18922d93d 100755 --- a/smoketest/scripts/system/test_kernel_options.py +++ b/smoketest/scripts/system/test_kernel_options.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -14,28 +14,38 @@  # 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 gzip  import re  import os  import platform  import unittest -from vyos.utils.process import call -from vyos.utils.file import read_file -  kernel = platform.release() -config = read_file(f'/boot/config-{kernel}') -CONFIG = '/proc/config.gz'  class TestKernelModules(unittest.TestCase):      """ VyOS makes use of a lot of Kernel drivers, modules and features. The      required modules which are essential for VyOS should be tested that they are      available in the Kernel that is run. """ +    _config_data = None + +    @classmethod +    def setUpClass(cls): +        import gzip +        from vyos.utils.process import call + +        super(TestKernelModules, cls).setUpClass() +        CONFIG = '/proc/config.gz' + +        if not os.path.isfile(CONFIG): +            call('sudo modprobe configs') + +        with gzip.open(CONFIG, 'rt') as f: +            cls._config_data = f.read() +      def test_bond_interface(self):          # The bond/lacp interface must be enabled in the OS Kernel          for option in ['CONFIG_BONDING']: -            tmp = re.findall(f'{option}=(y|m)', config) +            tmp = re.findall(f'{option}=(y|m)', self._config_data)              self.assertTrue(tmp)      def test_bridge_interface(self): @@ -43,7 +53,7 @@ class TestKernelModules(unittest.TestCase):          for option in ['CONFIG_BRIDGE',                         'CONFIG_BRIDGE_IGMP_SNOOPING',                         'CONFIG_BRIDGE_VLAN_FILTERING']: -            tmp = re.findall(f'{option}=(y|m)', config) +            tmp = re.findall(f'{option}=(y|m)', self._config_data)              self.assertTrue(tmp)      def test_dropmon_enabled(self): @@ -53,47 +63,53 @@ class TestKernelModules(unittest.TestCase):              'CONFIG_BPF_EVENTS=y',              'CONFIG_TRACEPOINTS=y'          ] -        if not os.path.isfile(CONFIG): -            call('sudo modprobe configs') -        with gzip.open(CONFIG, 'rt') as f: -            config_data = f.read()          for option in options_to_check: -            self.assertIn(option, config_data, -                          f"Option {option} is not present in /proc/config.gz") +            self.assertIn(option, self._config_data)      def test_synproxy_enabled(self):          options_to_check = [              'CONFIG_NFT_SYNPROXY',              'CONFIG_IP_NF_TARGET_SYNPROXY'          ] -        if not os.path.isfile(CONFIG): -            call('sudo modprobe configs') -        with gzip.open(CONFIG, 'rt') as f: -            config_data = f.read()          for option in options_to_check: -            tmp = re.findall(f'{option}=(y|m)', config_data) +            tmp = re.findall(f'{option}=(y|m)', self._config_data)              self.assertTrue(tmp)      def test_qemu_support(self): -        for option in ['CONFIG_VIRTIO_BLK', 'CONFIG_SCSI_VIRTIO', -                       'CONFIG_VIRTIO_NET', 'CONFIG_VIRTIO_CONSOLE', -                       'CONFIG_VIRTIO', 'CONFIG_VIRTIO_PCI', -                       'CONFIG_VIRTIO_BALLOON', 'CONFIG_CRYPTO_DEV_VIRTIO', -                       'CONFIG_X86_PLATFORM_DEVICES']: -            tmp = re.findall(f'{option}=(y|m)', config) +        options_to_check = [ +            'CONFIG_VIRTIO_BLK', 'CONFIG_SCSI_VIRTIO', +            'CONFIG_VIRTIO_NET', 'CONFIG_VIRTIO_CONSOLE', +            'CONFIG_VIRTIO', 'CONFIG_VIRTIO_PCI', +            'CONFIG_VIRTIO_BALLOON', 'CONFIG_CRYPTO_DEV_VIRTIO', +            'CONFIG_X86_PLATFORM_DEVICES' +            ] +        for option in options_to_check: +            tmp = re.findall(f'{option}=(y|m)', self._config_data)              self.assertTrue(tmp)      def test_vmware_support(self):          for option in ['CONFIG_VMXNET3']: -            tmp = re.findall(f'{option}=(y|m)', config) +            tmp = re.findall(f'{option}=(y|m)', self._config_data)              self.assertTrue(tmp)      def test_container_cgroup_support(self): -        for option in ['CONFIG_CGROUPS', 'CONFIG_MEMCG', 'CONFIG_CGROUP_PIDS', 'CONFIG_CGROUP_BPF']: -            tmp = re.findall(f'{option}=(y|m)', config) +        options_to_check = [ +            'CONFIG_CGROUPS', 'CONFIG_MEMCG', +            'CONFIG_CGROUP_PIDS', 'CONFIG_CGROUP_BPF' +            ] +        for option in options_to_check: +            tmp = re.findall(f'{option}=(y|m)', self._config_data) +            self.assertTrue(tmp) + +    def test_ip_routing_support(self): +        options_to_check = [ +            'CONFIG_IP_ADVANCED_ROUTER', 'CONFIG_IP_MULTIPLE_TABLES', +            'CONFIG_IP_ROUTE_MULTIPATH' +            ] +        for option in options_to_check: +            tmp = re.findall(f'{option}=(y|m)', self._config_data)              self.assertTrue(tmp)  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/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 371b219c0..5e5d5fba1 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -33,6 +33,7 @@ from vyos.ifconfig import BondIf  from vyos.ifconfig.ethernet import EthernetIf  from vyos.ifconfig import Section  from vyos.template import render_to_string +from vyos.utils.assertion import assert_mac  from vyos.utils.dict import dict_search  from vyos.utils.dict import dict_to_paths_values  from vyos.utils.network import interface_exists @@ -244,6 +245,16 @@ def verify(bond):              raise ConfigError('primary interface only works for mode active-backup, ' \                                'transmit-load-balance or adaptive-load-balance') +    if 'system_mac' in bond: +        if bond['mode'] != '802.3ad': +            raise ConfigError('Actor MAC address only available in 802.3ad mode!') + +        system_mac = bond['system_mac'] +        try: +            assert_mac(system_mac, test_all_zero=False) +        except: +            raise ConfigError(f'Cannot use a multicast MAC address "{system_mac}" as system-mac!') +      return None  def generate(bond): diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py index 9789f7bd3..7b2c1ee0b 100755 --- a/src/conf_mode/interfaces_bridge.py +++ b/src/conf_mode/interfaces_bridge.py @@ -56,6 +56,17 @@ def get_config(config=None):              bridge['member'].update({'interface_remove' : tmp })          else:              bridge.update({'member' : {'interface_remove' : tmp }}) +            for interface in tmp: +                # When using VXLAN member interfaces that are configured for Single +                # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to +                # re-create VLAN to VNI mappings if required, but only if the interface +                # is already live on the system - this must not be done on first commit +                if interface.startswith('vxlan') and interface_exists(interface): +                    set_dependents('vxlan', conf, interface) +                # When using Wireless member interfaces we need to inform hostapd +                # to properly set-up the bridge +                elif interface.startswith('wlan') and interface_exists(interface): +                    set_dependents('wlan', conf, interface)      if dict_search('member.interface', bridge) is not None:          for interface in list(bridge['member']['interface']): @@ -91,6 +102,10 @@ def get_config(config=None):              # is already live on the system - this must not be done on first commit              if interface.startswith('vxlan') and interface_exists(interface):                  set_dependents('vxlan', conf, interface) +            # When using Wireless member interfaces we need to inform hostapd +            # to properly set-up the bridge +            elif interface.startswith('wlan') and interface_exists(interface): +                set_dependents('wlan', conf, interface)      # delete empty dictionary keys - no need to run code paths if nothing is there to do      if 'member' in bridge: @@ -140,9 +155,6 @@ def verify(bridge):              if 'enable_vlan' in bridge:                  if 'has_vlan' in interface_config:                      raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!') - -                if 'wlan' in interface: -                    raise ConfigError(error_msg + 'VLAN aware cannot be set!')              else:                  for option in ['allowed_vlan', 'native_vlan']:                      if option in interface_config: @@ -168,12 +180,19 @@ def apply(bridge):      else:          br.update(bridge) -    for interface in dict_search('member.interface', bridge) or []: -        if interface.startswith('vxlan') and interface_exists(interface): +    tmp = [] +    if 'member' in bridge: +        if 'interface_remove' in bridge['member']: +            tmp.extend(bridge['member']['interface_remove']) +        if 'interface' in bridge['member']: +            tmp.extend(bridge['member']['interface']) + +    for interface in tmp: +        if interface.startswith(tuple(['vxlan', 'wlan'])) and interface_exists(interface):              try:                  call_dependents()              except ConfigError: -                raise ConfigError('Error in updating VXLAN interface after changing bridge!') +                raise ConfigError('Error updating member interface configuration after changing bridge!')      return None diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index 6da7e6a69..54d0669cb 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -41,6 +41,7 @@ from vyos.pki import encode_certificate  from vyos.pki import load_certificate  from vyos.pki import wrap_private_key  from vyos.template import render +from vyos.template import render_to_string  from vyos.utils.process import call  from vyos.utils.dict import dict_search  from vyos.utils.dict import dict_to_paths_values @@ -48,6 +49,7 @@ from vyos.utils.dict import dict_set  from vyos.utils.dict import dict_delete  from vyos.utils.file import write_file  from vyos import ConfigError +from vyos import frr  from vyos import airbag  airbag.enable() @@ -389,6 +391,10 @@ def generate(ethernet):              write_file(ca_cert_file_path, '\n'.join(ca_chains)) +    ethernet['frr_zebra_config'] = '' +    if 'deleted' not in ethernet: +        ethernet['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', ethernet) +      return None  def apply(ethernet): @@ -407,6 +413,17 @@ def apply(ethernet):      call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}') +    zebra_daemon = 'zebra' +    # Save original configuration prior to starting any commit actions +    frr_cfg = frr.FRRConfig() + +    # The route-map used for the FIB (zebra) is part of the zebra daemon +    frr_cfg.load_configuration(zebra_daemon) +    frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True) +    if 'frr_zebra_config' in ethernet: +        frr_cfg.add_before(frr.default_add_before, ethernet['frr_zebra_config']) +    frr_cfg.commit_configuration(zebra_daemon) +  if __name__ == '__main__':      try:          c = get_config() diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py index 1569d8d71..a4efb1cd8 100755 --- a/src/conf_mode/load-balancing_reverse-proxy.py +++ b/src/conf_mode/load-balancing_reverse-proxy.py @@ -88,6 +88,12 @@ def verify(lb):              if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf):                  raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"') +    # Check if http-response-headers are configured in any frontend/backend where mode != http +    for group in ['service', 'backend']: +        for config_name, config in lb[group].items(): +            if 'http_response_headers' in config and ('mode' not in config or config['mode'] != 'http'): +                raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!') +          if 'ssl' in back_config:              if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']):                  raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!') diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 4cd9b570d..f74bb217e 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -17,7 +17,6 @@  import os  from sys import exit -from netifaces import interfaces  from vyos.base import Warning  from vyos.config import Config @@ -30,6 +29,7 @@ from vyos.utils.dict import dict_search_args  from vyos.utils.process import cmd  from vyos.utils.process import run  from vyos.utils.network import is_addr_assigned +from vyos.utils.network import interface_exists  from vyos import ConfigError  from vyos import airbag @@ -149,8 +149,12 @@ def verify(nat):                  if 'name' in config['outbound_interface'] and 'group' in config['outbound_interface']:                      raise ConfigError(f'{err_msg} cannot specify both interface group and interface name for nat source rule "{rule}"')                  elif 'name' in config['outbound_interface']: -                    if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces(): -                        Warning(f'NAT interface "{config["outbound_interface"]["name"]}" for source NAT rule "{rule}" does not exist!') +                    interface_name = config['outbound_interface']['name'] +                    if interface_name not in 'any': +                        if interface_name.startswith('!'): +                            interface_name = interface_name[1:] +                        if not interface_exists(interface_name): +                            Warning(f'Interface "{interface_name}" for source NAT rule "{rule}" does not exist!')                  else:                      group_name = config['outbound_interface']['group']                      if group_name[0] == '!': @@ -182,8 +186,12 @@ def verify(nat):                  if 'name' in config['inbound_interface'] and 'group' in config['inbound_interface']:                      raise ConfigError(f'{err_msg} cannot specify both interface group and interface name for destination nat rule "{rule}"')                  elif 'name' in config['inbound_interface']: -                    if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces(): -                        Warning(f'NAT interface "{config["inbound_interface"]["name"]}" for destination NAT rule "{rule}" does not exist!') +                    interface_name = config['inbound_interface']['name'] +                    if interface_name not in 'any': +                        if interface_name.startswith('!'): +                            interface_name = interface_name[1:] +                        if not interface_exists(interface_name): +                            Warning(f'Interface "{interface_name}" for destination NAT rule "{rule}" does not exist!')                  else:                      group_name = config['inbound_interface']['group']                      if group_name[0] == '!': diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index fe017527d..075738dad 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -17,15 +17,15 @@  import os  from sys import exit -from netifaces import interfaces  from vyos.base import Warning  from vyos.config import Config  from vyos.configdep import set_dependents, call_dependents  from vyos.template import render -from vyos.utils.process import cmd -from vyos.utils.kernel import check_kmod  from vyos.utils.dict import dict_search +from vyos.utils.kernel import check_kmod +from vyos.utils.network import interface_exists +from vyos.utils.process import cmd  from vyos.template import is_ipv6  from vyos import ConfigError  from vyos import airbag @@ -64,8 +64,12 @@ def verify(nat):                  if 'name' in config['outbound_interface'] and 'group' in config['outbound_interface']:                      raise ConfigError(f'{err_msg} cannot specify both interface group and interface name for nat source rule "{rule}"')                  elif 'name' in config['outbound_interface']: -                    if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces(): -                        Warning(f'NAT66 interface "{config["outbound_interface"]["name"]}" for source NAT66 rule "{rule}" does not exist!') +                    interface_name = config['outbound_interface']['name'] +                    if interface_name not in 'any': +                        if interface_name.startswith('!'): +                            interface_name = interface_name[1:] +                        if not interface_exists(interface_name): +                            Warning(f'Interface "{interface_name}" for source NAT66 rule "{rule}" does not exist!')              addr = dict_search('translation.address', config)              if addr != None: @@ -88,8 +92,12 @@ def verify(nat):                  if 'name' in config['inbound_interface'] and 'group' in config['inbound_interface']:                      raise ConfigError(f'{err_msg} cannot specify both interface group and interface name for destination nat rule "{rule}"')                  elif 'name' in config['inbound_interface']: -                    if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces(): -                        Warning(f'NAT66 interface "{config["inbound_interface"]["name"]}" for destination NAT66 rule "{rule}" does not exist!') +                    interface_name = config['inbound_interface']['name'] +                    if interface_name not in 'any': +                        if interface_name.startswith('!'): +                            interface_name = interface_name[1:] +                        if not interface_exists(interface_name): +                            Warning(f'Interface "{interface_name}" for destination NAT66 rule "{rule}" does not exist!')      return None 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/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 4df97d133..22f020099 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -31,6 +31,7 @@ from vyos.utils.dict import dict_search  from vyos.utils.network import get_interface_vrf  from vyos.utils.network import is_addr_assigned  from vyos.utils.process import process_named_running +from vyos.utils.process import call  from vyos import ConfigError  from vyos import frr  from vyos import airbag @@ -50,13 +51,8 @@ def get_config(config=None):      # eqivalent of the C foo ? 'a' : 'b' statement      base = vrf and ['vrf', 'name', vrf, 'protocols', 'bgp'] or base_path -    bgp = conf.get_config_dict( -        base, -        key_mangling=('-', '_'), -        get_first_key=True, -        no_tag_node_value_mangle=True, -        with_recursive_defaults=True, -    ) +    bgp = conf.get_config_dict(base, key_mangling=('-', '_'), +                               get_first_key=True, no_tag_node_value_mangle=True)      bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],                                                   key_mangling=('-', '_'), @@ -75,22 +71,29 @@ def get_config(config=None):      if vrf:          bgp.update({'vrf' : vrf})          # We can not delete the BGP VRF instance if there is a L3VNI configured +        # FRR L3VNI must be deleted first otherwise we will see error: +        # "FRR error: Please unconfigure l3vni 3000"          tmp = ['vrf', 'name', vrf, 'vni'] -        if conf.exists(tmp): -            bgp.update({'vni' : conf.return_value(tmp)}) +        if conf.exists_effective(tmp): +            bgp.update({'vni' : conf.return_effective_value(tmp)})          # We can safely delete ourself from the dependent vrf list          if vrf in bgp['dependent_vrfs']:              del bgp['dependent_vrfs'][vrf] -    bgp['dependent_vrfs'].update({'default': {'protocols': { -        'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'), -                                    get_first_key=True, -                                    no_tag_node_value_mangle=True)}}}) +        bgp['dependent_vrfs'].update({'default': {'protocols': { +            'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'), +                                        get_first_key=True, +                                        no_tag_node_value_mangle=True)}}}) +      if not conf.exists(base):          # If bgp instance is deleted then mark it          bgp.update({'deleted' : ''})          return bgp +    # We have gathered the dict representation of the CLI, but there are default +    # options which we need to update into the dictionary retrived. +    bgp = conf.merge_defaults(bgp, recursive=True) +      # We also need some additional information from the config, prefix-lists      # and route-maps for instance. They will be used in verify().      # @@ -242,10 +245,6 @@ def verify(bgp):                  if verify_vrf_as_import(bgp['vrf'], tmp_afi, bgp['dependent_vrfs']):                      raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \                                        'unconfigure "import vrf" commands!') -            # We can not delete the BGP instance if a L3VNI instance exists -            if 'vni' in bgp: -                raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \ -                                  f'unconfigure VNI "{bgp["vni"]}" first!')          else:              # We are running in the default VRF context, thus we can not delete              # our main BGP instance if there are dependent BGP VRF instances. @@ -254,7 +253,11 @@ def verify(bgp):                      if vrf != 'default':                          if dict_search('protocols.bgp', vrf_options):                              raise ConfigError('Cannot delete default BGP instance, ' \ -                                              'dependent VRF instance(s) exist!') +                                              'dependent VRF instance(s) exist(s)!') +                        if 'vni' in vrf_options: +                            raise ConfigError('Cannot delete default BGP instance, ' \ +                                              'dependent L3VNI exists!') +          return None      if 'system_as' not in bgp: @@ -330,7 +333,7 @@ def verify(bgp):                      raise ConfigError('Cannot have local-as same as system-as number')                  # Neighbor AS specified for local-as and remote-as can not be the same -                if dict_search('remote_as', peer_config) == asn: +                if dict_search('remote_as', peer_config) == asn and neighbor != 'peer_group':                       raise ConfigError(f'Neighbor "{peer}" has local-as specified which is '\                                          'the same as remote-as, this is not allowed!') @@ -607,6 +610,13 @@ def generate(bgp):      return None  def apply(bgp): +    if 'deleted' in bgp: +        # We need to ensure that the L3VNI is deleted first. +        # This is not possible with old config backend +        # priority bug +        if {'vrf', 'vni'} <= set(bgp): +            call('vtysh -c "conf t" -c "vrf {vrf}" -c "no vni {vni}"'.format(**bgp)) +      bgp_daemon = 'bgpd'      # Save original configuration prior to starting any commit actions diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index ccfc8f6b8..8a590cbc6 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -39,6 +39,9 @@ from vyos.utils.dict import dict_search_recursive  from vyos.utils.process import run  from vyos import ConfigError  from vyos import airbag +from vyos.xml_ref import relative_defaults + +  airbag.enable()  map_vyops_tc = { @@ -115,8 +118,18 @@ def get_config(config=None):              for rd_name in list(qos['policy'][policy]):                  # There are eight precedence levels - ensure all are present                  # to be filled later down with the appropriate default values -                default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {}, -                                                       '4' : {}, '5' : {}, '6' : {}, '7' : {} }} +                default_p_val = relative_defaults( +                    ['qos', 'policy', 'random-detect', rd_name, 'precedence'], +                    {'precedence': {'0': {}}}, +                    get_first_key=True, recursive=True +                )['0'] +                default_p_val = {key.replace('-', '_'): value for key, value in default_p_val.items()} +                default_precedence = { +                    'precedence': {'0': default_p_val, '1': default_p_val, +                                   '2': default_p_val, '3': default_p_val, +                                   '4': default_p_val, '5': default_p_val, +                                   '6': default_p_val, '7': default_p_val}} +                  qos['policy']['random_detect'][rd_name] = dict_merge(                      default_precedence, qos['policy']['random_detect'][rd_name]) @@ -124,18 +137,6 @@ def get_config(config=None):      for policy in qos.get('policy', []):          for p_name, p_config in qos['policy'][policy].items(): -            if 'precedence' in p_config: -                # precedence settings are a bit more complex as they are -                # calculated under specific circumstances: -                for precedence in p_config['precedence']: -                    max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold']) -                    if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]: -                        qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str( -                            int((9 + int(precedence)) * max_thr) // 18); - -                    if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]: -                        qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \ -                            str(int(4 * max_thr))              # cleanup empty match config              if 'class' in p_config:                  for cls, cls_config in p_config['class'].items(): diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py index c7333dd3a..7af88007c 100755 --- a/src/conf_mode/service_dhcpv6-server.py +++ b/src/conf_mode/service_dhcpv6-server.py @@ -106,14 +106,14 @@ def verify(dhcpv6):                          # Stop address must be greater or equal to start address                          if not ip_address(stop) >= ip_address(start): -                            raise ConfigError(f'Range stop address "{stop}" must be greater then or equal ' \ +                            raise ConfigError(f'Range stop address "{stop}" must be greater than or equal ' \                                                f'to the range start address "{start}"!')                          # DHCPv6 range start address must be unique - two ranges can't                          # start with the same address - makes no sense                          if start in range6_start:                              raise ConfigError(f'Conflicting DHCPv6 lease range: '\ -                                              f'Pool start address "{start}" defined multipe times!') +                                              f'Pool start address "{start}" defined multiple times!')                          range6_start.append(start) @@ -121,7 +121,7 @@ def verify(dhcpv6):                          # end with the same address - makes no sense                          if stop in range6_stop:                              raise ConfigError(f'Conflicting DHCPv6 lease range: '\ -                                              f'Pool stop address "{stop}" defined multipe times!') +                                              f'Pool stop address "{stop}" defined multiple times!')                          range6_stop.append(stop) @@ -180,7 +180,7 @@ def verify(dhcpv6):              if 'option' in subnet_config:                  if 'vendor_option' in subnet_config['option']:                      if len(dict_search('option.vendor_option.cisco.tftp_server', subnet_config)) > 2: -                        raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!') +                        raise ConfigError(f'No more than two Cisco tftp-servers should be defined for subnet "{subnet}"!')              # Subnets must be unique              if subnet in subnets: diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 328487985..c95f976d3 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -84,12 +84,29 @@ def verify_pado_delay(pppoe):          pado_delay = pppoe['pado_delay']          delays_without_sessions = pado_delay['delays_without_sessions'] +        if 'disable' in delays_without_sessions: +            raise ConfigError( +                'Number of sessions must be specified for "pado-delay disable"' +            ) +          if len(delays_without_sessions) > 1:              raise ConfigError(                  f'Cannot add more then ONE pado-delay without sessions, '                  f'but {len(delays_without_sessions)} were set'              ) +        if 'disable' in [delay[0] for delay in pado_delay['delays_with_sessions']]: +            # need to sort delays by sessions to verify if there is no delay +            # for sessions after disabling +            sorted_pado_delay = sorted(pado_delay['delays_with_sessions'], key=lambda k_v: k_v[1]) +            last_delay = sorted_pado_delay[-1] + +            if last_delay[0] != 'disable': +                raise ConfigError( +                    f'Cannot add pado-delay after disabled sessions, but ' +                    f'"pado-delay {last_delay[0]} sessions {last_delay[1]}" was set' +                ) +  def verify(pppoe):      if not pppoe:          return None diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py deleted file mode 100755 index 0df8dc09e..000000000 --- a/src/conf_mode/service_upnp.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021-2022 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 - -from sys import exit -import uuid -import netifaces -from ipaddress import IPv4Network -from ipaddress import IPv6Network - -from vyos.config import Config -from vyos.utils.process import call -from vyos.template import render -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -config_file = r'/run/upnp/miniupnp.conf' - -def get_config(config=None): -    if config: -        conf = config -    else: -        conf = Config() - -    base = ['service', 'upnp'] -    upnpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - -    if not upnpd: -        return None - -    upnpd = conf.merge_defaults(upnpd, recursive=True) - -    uuidgen = uuid.uuid1() -    upnpd.update({'uuid': uuidgen}) - -    return upnpd - -def get_all_interface_addr(prefix, filter_dev, filter_family): -    list_addr = [] -    for interface in netifaces.interfaces(): -        if filter_dev and interface in filter_dev: -            continue -        addrs = netifaces.ifaddresses(interface) -        if netifaces.AF_INET in addrs.keys(): -            if netifaces.AF_INET in filter_family: -                for addr in addrs[netifaces.AF_INET]: -                    if prefix: -                        # we need to manually assemble a list of IPv4 address/prefix -                        prefix = '/' + \ -                            str(IPv4Network('0.0.0.0/' + addr['netmask']).prefixlen) -                        list_addr.append(addr['addr'] + prefix) -                    else: -                        list_addr.append(addr['addr']) -        if netifaces.AF_INET6 in addrs.keys(): -            if netifaces.AF_INET6 in filter_family: -                for addr in addrs[netifaces.AF_INET6]: -                    if prefix: -                        # we need to manually assemble a list of IPv4 address/prefix -                        bits = bin(int(addr['netmask'].replace(':', '').split('/')[0], 16)).count('1') -                        prefix = '/' + str(bits) -                        list_addr.append(addr['addr'] + prefix) -                    else: -                        list_addr.append(addr['addr']) - -    return list_addr - -def verify(upnpd): -    if not upnpd: -        return None - -    if 'wan_interface' not in upnpd: -        raise ConfigError('To enable UPNP, you must have the "wan-interface" option!') - -    if 'rule' in upnpd: -        for rule, rule_config in upnpd['rule'].items(): -            for option in ['external_port_range', 'internal_port_range', 'ip', 'action']: -                if option not in rule_config: -                    tmp = option.replace('_', '-') -                    raise ConfigError(f'Every UPNP rule requires "{tmp}" to be set!') - -    if 'stun' in upnpd: -        for option in ['host', 'port']: -            if option not in upnpd['stun']: -                raise ConfigError(f'A UPNP stun support must have an "{option}" option!') - -    # Check the validity of the IP address -    listen_dev = [] -    system_addrs_cidr = get_all_interface_addr(True, [], [netifaces.AF_INET, netifaces.AF_INET6]) -    system_addrs = get_all_interface_addr(False, [], [netifaces.AF_INET, netifaces.AF_INET6]) -    if 'listen' not in upnpd: -        raise ConfigError(f'Listen address or interface is required!') -    for listen_if_or_addr in upnpd['listen']: -        if listen_if_or_addr not in netifaces.interfaces(): -            listen_dev.append(listen_if_or_addr) -        if (listen_if_or_addr not in system_addrs) and (listen_if_or_addr not in system_addrs_cidr) and \ -                (listen_if_or_addr not in netifaces.interfaces()): -            if is_ipv4(listen_if_or_addr) and IPv4Network(listen_if_or_addr).is_multicast: -                raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed' -                                  f'to listen on. It is not an interface address nor a multicast address!') -            if is_ipv6(listen_if_or_addr) and IPv6Network(listen_if_or_addr).is_multicast: -                raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed' -                                  f'to listen on. It is not an interface address nor a multicast address!') - -    system_listening_dev_addrs_cidr = get_all_interface_addr(True, listen_dev, [netifaces.AF_INET6]) -    system_listening_dev_addrs = get_all_interface_addr(False, listen_dev, [netifaces.AF_INET6]) -    for listen_if_or_addr in upnpd['listen']: -        if listen_if_or_addr not in netifaces.interfaces() and \ -                (listen_if_or_addr not in system_listening_dev_addrs_cidr) and \ -                (listen_if_or_addr not in system_listening_dev_addrs) and \ -                is_ipv6(listen_if_or_addr) and \ -                (not IPv6Network(listen_if_or_addr).is_multicast): -            raise ConfigError(f'{listen_if_or_addr} must listen on the interface of the network card') - -def generate(upnpd): -    if not upnpd: -        return None - -    if os.path.isfile(config_file): -        os.unlink(config_file) - -    render(config_file, 'firewall/upnpd.conf.j2', upnpd) - -def apply(upnpd): -    systemd_service_name = 'miniupnpd.service' -    if not upnpd: -        # Stop the UPNP service -        call(f'systemctl stop {systemd_service_name}') -    else: -        # Start the UPNP service -        call(f'systemctl restart {systemd_service_name}') - -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_host-name.py b/src/conf_mode/system_host-name.py index 8975cadb6..3f245f166 100755 --- a/src/conf_mode/system_host-name.py +++ b/src/conf_mode/system_host-name.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-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 @@ -22,6 +22,7 @@ import vyos.hostsd_client  from vyos.base import Warning  from vyos.config import Config +from vyos.configdict import leaf_node_changed  from vyos.ifconfig import Section  from vyos.template import is_ip  from vyos.utils.process import cmd @@ -37,6 +38,7 @@ default_config_data = {      'domain_search': [],      'nameserver': [],      'nameservers_dhcp_interfaces': {}, +    'snmpd_restart_reqired': False,      'static_host_mapping': {}  } @@ -52,6 +54,10 @@ def get_config(config=None):      hosts['hostname'] = conf.return_value(['system', 'host-name']) +    base = ['system'] +    if leaf_node_changed(conf, base + ['host-name']) or leaf_node_changed(conf, base + ['domain-name']): +        hosts['snmpd_restart_reqired'] = True +      # This may happen if the config is not loaded yet,      # e.g. if run by cloud-init      if not hosts['hostname']: @@ -171,7 +177,7 @@ def apply(config):          call("systemctl restart rsyslog.service")      # If SNMP is running, restart it too -    if process_named_running('snmpd'): +    if process_named_running('snmpd') and config['snmpd_restart_reqired']:          call('systemctl restart snmpd.service')      return None diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 587309005..8d8c234c0 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -130,11 +130,6 @@ def get_config(config=None):      tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],                                                            get_first_key=True)}} -    # L3VNI setup is done via vrf_vni.py as it must be de-configured (on node -    # deletetion prior to the BGP process. Tell the Jinja2 template no VNI -    # setup is needed -    vrf.update({'no_vni' : ''}) -      # Merge policy dict into "regular" config dict      vrf = dict_merge(tmp, vrf)      return vrf diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py deleted file mode 100644 index 8dab164d7..000000000 --- a/src/conf_mode/vrf_vni.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2023-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 argv -from sys import exit - -from vyos.config import Config -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() - -    vrf_name = None -    if len(argv) > 1: -        vrf_name = argv[1] -    else: -        return None - -    # Using duplicate L3VNIs makes no sense - it's also forbidden in FRR, -    # thus VyOS CLI must deny this, too. Instead of getting only the dict for -    # the requested VRF and den comparing it with depenent VRfs to not have any -    # duplicate we will just grad ALL VRFs by default but only render/apply -    # the configuration for the requested VRF - that makes the code easier and -    # hopefully less error prone -    vrf = conf.get_config_dict(['vrf'], key_mangling=('-', '_'), -                               no_tag_node_value_mangle=True, -                               get_first_key=True) - -    # Store name of VRF we are interested in for FRR config rendering -    vrf.update({'only_vrf' : vrf_name}) - -    return vrf - -def verify(vrf): -    if not vrf: -        return - -    if len(argv) < 2: -        raise ConfigError('VRF parameter not specified when valling vrf_vni.py') - -    if 'name' in vrf: -        vni_ids = [] -        for name, vrf_config in vrf['name'].items(): -            # VRF VNI (Virtual Network Identifier) must be unique on the system -            if 'vni' in vrf_config: -                if vrf_config['vni'] in vni_ids: -                    raise ConfigError(f'VRF "{name}" VNI is not unique!') -                vni_ids.append(vrf_config['vni']) - -    return None - -def generate(vrf): -    if not vrf: -        return - -    vrf['new_frr_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) -    return None - -def apply(vrf): -    frr_daemon = 'zebra' - -    # add configuration to FRR -    frr_cfg = frr.FRRConfig() -    frr_cfg.load_configuration(frr_daemon) -    # There is only one VRF inside the dict as we read only one in get_config() -    if vrf and 'only_vrf' in vrf: -        vrf_name = vrf['only_vrf'] -        frr_cfg.modify_section(f'^vrf {vrf_name}', stop_pattern='^exit-vrf', remove_stop_mark=True) -    if vrf and 'new_frr_config' in vrf: -        frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config']) -    frr_cfg.commit_configuration(frr_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/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 0604b2837..9d9aec376 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -93,7 +93,8 @@ def set_remote_config(          key: str,          op: str,          mask: Dict[str, Any], -        config: Dict[str, Any]) -> Optional[Dict[str, Any]]: +        config: Dict[str, Any], +        port: int) -> Optional[Dict[str, Any]]:      """Loads the VyOS configuration in JSON format to a remote host.      Args: @@ -102,6 +103,7 @@ def set_remote_config(          op (str): The operation to perform (set or load).          mask (dict): The dict of paths in sections.          config (dict): The dict of masked config data. +        port (int): The remote API port      Returns:          Optional[Dict[str, Any]]: The response from the remote host as a @@ -113,7 +115,7 @@ def set_remote_config(      # Disable the InsecureRequestWarning      urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -    url = f'https://{address}/configure-section' +    url = f'https://{address}:{port}/configure-section'      data = json.dumps({          'op': op,          'mask': mask, @@ -138,7 +140,8 @@ def is_section_revised(section: List[str]) -> bool:  def config_sync(secondary_address: str,                  secondary_key: str,                  sections: List[list[str]], -                mode: str): +                mode: str, +                secondary_port: int):      """Retrieve a config section from primary router in JSON format and send it to         secondary router      """ @@ -158,7 +161,8 @@ def config_sync(secondary_address: str,                                     key=secondary_key,                                     op=mode,                                     mask=mask_dict, -                                   config=config_dict) +                                   config=config_dict, +                                   port=secondary_port)      logger.debug(f"Set config for sections '{sections}': {set_config}") @@ -178,14 +182,12 @@ if __name__ == '__main__':      secondary_address = config.get('secondary', {}).get('address')      secondary_address = bracketize_ipv6(secondary_address)      secondary_key = config.get('secondary', {}).get('key') +    secondary_port = int(config.get('secondary', {}).get('port', 443))      sections = config.get('section')      timeout = int(config.get('secondary', {}).get('timeout')) -    if not all([ -            mode, secondary_address, secondary_key, sections -    ]): -        logger.error( -            "Missing required configuration data for config synchronization.") +    if not all([mode, secondary_address, secondary_key, sections]): +        logger.error("Missing required configuration data for config synchronization.")          exit(0)      # Generate list_sections of sections/subsections @@ -200,5 +202,4 @@ if __name__ == '__main__':          else:              list_sections.append([section]) -    config_sync(secondary_address, secondary_key, -                list_sections, mode) +    config_sync(secondary_address, secondary_key, list_sections, mode, secondary_port) diff --git a/src/migration-scripts/nat/7-to-8 b/src/migration-scripts/nat/7-to-8 new file mode 100755 index 000000000..ab2ffa6d3 --- /dev/null +++ b/src/migration-scripts/nat/7-to-8 @@ -0,0 +1,62 @@ +#!/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/>. + +# T6345: random - In kernel 5.0 and newer this is the same as fully-random. +#        In earlier kernels the port mapping will be randomized using a seeded +#        MD5 hash mix using source and destination address and destination port. +#        drop fully-random from CLI + +from sys import argv,exit +from vyos.configtree import ConfigTree + +if len(argv) < 2: +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['nat']): +    # Nothing to do +    exit(0) + +for direction in ['source', 'destination']: +    # If a node doesn't exist, we obviously have nothing to do. +    if not config.exists(['nat', direction]): +        continue + +    # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist, +    # but there are no rules under it. +    if not config.list_nodes(['nat', direction]): +        continue + +    for rule in config.list_nodes(['nat', direction, 'rule']): +        port_mapping = ['nat', direction, 'rule', rule, 'translation', 'options', 'port-mapping'] +        if config.exists(port_mapping): +            tmp = config.return_value(port_mapping) +            if tmp == 'fully-random': +                config.set(port_mapping, value='random') + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) diff --git a/src/migration-scripts/pppoe-server/9-to-10 b/src/migration-scripts/pppoe-server/9-to-10 new file mode 100755 index 000000000..e0c782f04 --- /dev/null +++ b/src/migration-scripts/pppoe-server/9-to-10 @@ -0,0 +1,56 @@ +#!/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/>. + +# Migration of pado-delay options + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + +if len(argv) < 2: +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'pppoe-server', 'pado-delay'] +if not config.exists(base): +    exit(0) + +pado_delay = {} +for delay in config.list_nodes(base): +    sessions = config.return_value(base + [delay, 'sessions']) +    pado_delay[delay] = sessions + +# need to define delay for latest sessions +sorted_delays = dict(sorted(pado_delay.items(), key=lambda k_v: int(k_v[1]))) +last_delay = list(sorted_delays)[-1] + +# Rename last delay -> disable +tmp = base + [last_delay] +if config.exists(tmp): +    config.rename(tmp, 'disable') + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) diff --git a/src/op_mode/bonding.py b/src/op_mode/bonding.py new file mode 100755 index 000000000..07bccbd4b --- /dev/null +++ b/src/op_mode/bonding.py @@ -0,0 +1,103 @@ +#!/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 will parse 'sudo cat /proc/net/bonding/<interface name>' and return table output for lacp related info + +import subprocess +import re +import sys +import typing +from tabulate import tabulate + +import vyos.opmode +from vyos.configquery import ConfigTreeQuery + +def list_to_dict(data, headers, basekey): +    data_list = {basekey: []} + +    for row in data: +        row_dict = {headers[i]: row[i] for i in range(len(headers))} +        data_list[basekey].append(row_dict) + +    return data_list + +def show_lacp_neighbors(raw: bool, interface: typing.Optional[str]): +    headers = ["Interface", "Member", "Local ID", "Remote ID"] +    data = subprocess.run(f"cat /proc/net/bonding/{interface}", stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True, text=False).stdout.decode('utf-8') +    if 'Bonding Mode: IEEE 802.3ad Dynamic link aggregation' not in data: +        raise vyos.opmode.DataUnavailable(f"{interface} is not present or not configured with mode 802.3ad") + +    pattern = re.compile( +        r"Slave Interface: (?P<member>\w+\d+).*?" +        r"system mac address: (?P<local_id>[0-9a-f:]+).*?" +        r"details partner lacp pdu:.*?" +        r"system mac address: (?P<remote_id>[0-9a-f:]+)", +        re.DOTALL +    ) + +    interfaces = [] + +    for match in re.finditer(pattern, data): +        member = match.group("member") +        local_id = match.group("local_id") +        remote_id = match.group("remote_id") +        interfaces.append([interface, member, local_id, remote_id]) + +    if raw: +        return list_to_dict(interfaces, headers, 'lacp') +    else: +        return tabulate(interfaces, headers) + +def show_lacp_detail(raw: bool, interface: typing.Optional[str]): +    headers = ["Interface", "Members", "Mode", "Rate", "System-MAC", "Hash"] +    query = ConfigTreeQuery() + +    if interface: +        intList = [interface] +    else: +        intList = query.list_nodes(['interfaces', 'bonding']) + +    bondList = [] + +    for interface in intList: +        data = subprocess.run(f"cat /proc/net/bonding/{interface}", stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True, text=False).stdout.decode('utf-8') +        if 'Bonding Mode: IEEE 802.3ad Dynamic link aggregation' not in data: +            continue + +        mode_active = "active" if "LACP active: on" in data else "passive" +        lacp_rate = re.search(r"LACP rate: (\w+)", data).group(1) if re.search(r"LACP rate: (\w+)", data) else "N/A" +        hash_policy = re.search(r"Transmit Hash Policy: (.+?) \(\d+\)", data).group(1) if re.search(r"Transmit Hash Policy: (.+?) \(\d+\)", data) else "N/A" +        system_mac = re.search(r"System MAC address: ([0-9a-f:]+)", data).group(1) if re.search(r"System MAC address: ([0-9a-f:]+)", data) else "N/A" +        if raw: +            members = re.findall(r"Slave Interface: ([a-zA-Z0-9:_-]+)", data) +        else: +            members = ",".join(set(re.findall(r"Slave Interface: ([a-zA-Z0-9:_-]+)", data))) + +        bondList.append([interface, members, mode_active, lacp_rate, system_mac, hash_policy]) + +    if raw: +        return list_to_dict(bondList, headers, 'lacp') +    else: +        return tabulate(bondList, headers) + +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/cgnat.py b/src/op_mode/cgnat.py new file mode 100755 index 000000000..9ad8f92f9 --- /dev/null +++ b/src/op_mode/cgnat.py @@ -0,0 +1,96 @@ +#!/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 +import typing + +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(external_address: str = '', internal_address: str = '') -> list[dict]: +    """Get CGNAT dictionary and filter by external or internal address if provided.""" +    cmd_output = cmd(f'nft --json list table ip {CGNAT_TABLE}') +    data = json.loads(cmd_output) + +    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}' + +        if (internal_address and internal != internal_address) or ( +            external_address and external != external_address +        ): +            continue + +        allocations.append( +            { +                'internal_address': internal, +                'external_address': external, +                'port_range': port_range, +            } +        ) + +    return allocations + + +def _get_formatted_output(allocations: list[dict]) -> str: +    # Convert the list of dictionaries to a list of tuples for tabulate +    headers = ['Internal IP', 'External IP', 'Port range'] +    data = [ +        (alloc['internal_address'], alloc['external_address'], alloc['port_range']) +        for alloc in allocations +    ] +    output = tabulate(data, headers, numalign="left") +    return output + + +def show_allocation( +    raw: bool, +    external_address: typing.Optional[str], +    internal_address: typing.Optional[str], +) -> str: +    config = ConfigTreeQuery() +    if not config.exists('nat cgnat'): +        raise vyos.opmode.UnconfiguredSubsystem('CGNAT is not configured') + +    if raw: +        return _get_raw_data(external_address, internal_address) + +    else: +        raw_data = _get_raw_data(external_address, internal_address) +        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/firewall.py b/src/op_mode/firewall.py index 442c186cc..15fbb65a2 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -531,9 +531,15 @@ def show_firewall_group(name=None):                              continue                          for idx, member in enumerate(members): -                            val = member.get('val', 'N/D') -                            timeout = str(member.get('timeout', 'N/D')) -                            expires = str(member.get('expires', 'N/D')) +                            if isinstance(member, str): +                                # Only member, and no timeout: +                                val = member +                                timeout = "N/D" +                                expires = "N/D" +                            else: +                                val = member.get('val', 'N/D') +                                timeout = str(member.get('timeout', 'N/D')) +                                expires = str(member.get('expires', 'N/D'))                              if args.detail:                                  row.append(f'{val} (timeout: {timeout}, expires: {expires})') diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index ba0e3b6db..0d2d7076c 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -23,6 +23,8 @@ from shutil import copy, chown, rmtree, copytree  from glob import glob  from sys import exit  from os import environ +from os import readlink +from os import getpid, getppid  from typing import Union  from urllib.parse import urlparse  from passlib.hosts import linux_context @@ -65,7 +67,7 @@ MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:'  MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:'  MSG_INPUT_ROOT_SIZE_ALL: str = 'Would you like to use all the free space on the drive?'  MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root partition (min is 1.5 GB)?' -MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?' +MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial)?'  MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?'  MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?'  MSG_INPUT_COPY_ENC_DATA: str = 'Would you like to copy the encrypted config to the new image?' @@ -614,6 +616,20 @@ def copy_ssh_host_keys() -> bool:      return False +def console_hint() -> str: +    pid = getppid() if 'SUDO_USER' in environ else getpid() +    try: +        path = readlink(f'/proc/{pid}/fd/1') +    except OSError: +        path = '/dev/tty' + +    name = Path(path).name +    if name == 'ttyS0': +        return 'S' +    else: +        return 'K' + +  def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None:      """Clean up after installation @@ -709,9 +725,9 @@ def install_image() -> None:      # ask for default console      console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, -                                  default='K', -                                  valid_responses=['K', 'S', 'U']) -    console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS', 'U': 'ttyUSB'} +                                  default=console_hint(), +                                  valid_responses=['K', 'S']) +    console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS'}      config_boot_list = ['/opt/vyatta/etc/config/config.boot',                          '/opt/vyatta/etc/config.boot.default'] diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py index 1cfb5f5a1..fb4286dbc 100755 --- a/src/op_mode/image_manager.py +++ b/src/op_mode/image_manager.py @@ -21,7 +21,7 @@ from argparse import ArgumentParser, Namespace  from pathlib import Path  from shutil import rmtree  from sys import exit -from typing import Optional +from typing import Optional, Literal, TypeAlias, get_args  from vyos.system import disk, grub, image, compat  from vyos.utils.io import ask_yes_no, select_entry @@ -33,6 +33,8 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:'  MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first'  MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first' +ConsoleType: TypeAlias = Literal['tty', 'ttyS'] +  def annotate_list(images_list: list[str]) -> list[str]:      """Annotate list of images with additional info @@ -202,6 +204,15 @@ def rename_image(name_old: str, name_new: str) -> None:              exit(f'Unable to rename the encrypted config for "{name_old}" to "{name_new}": {err}') +@compat.grub_cfg_update +def set_console_type(console_type: ConsoleType) -> None: +    console_choice = get_args(ConsoleType) +    if console_type not in console_choice: +        exit(f'console type \'{console_type}\' not available') + +    grub.set_console_type(console_type) + +  def list_images() -> None:      """Print list of available images for CLI hints"""      images_list: list[str] = grub.version_list() @@ -209,6 +220,13 @@ def list_images() -> None:          print(image_name) +def list_console_types() -> None: +    """Print list of console types for CLI hints""" +    console_types: list[str] = list(get_args(ConsoleType)) +    for console_type in console_types: +        print(console_type) + +  def parse_arguments() -> Namespace:      """Parse arguments @@ -217,7 +235,8 @@ def parse_arguments() -> Namespace:      """      parser: ArgumentParser = ArgumentParser(description='Manage system images')      parser.add_argument('--action', -                        choices=['delete', 'set', 'rename', 'list'], +                        choices=['delete', 'set', 'set_console_type', +                                 'rename', 'list', 'list_console_types'],                          required=True,                          help='action to perform with an image')      parser.add_argument('--no-prompt', action='store_true', @@ -227,6 +246,7 @@ def parse_arguments() -> Namespace:          help=          'a name of an image to add, delete, install, rename, or set as default')      parser.add_argument('--image-new-name', help='a new name for image') +    parser.add_argument('--console-type', help='console type for boot')      args: Namespace = parser.parse_args()      # Validate arguments      if args.action == 'rename' and (not args.image_name or @@ -243,10 +263,14 @@ if __name__ == '__main__':              delete_image(args.image_name, args.no_prompt)          if args.action == 'set':              set_image(args.image_name) +        if args.action == 'set_console_type': +            set_console_type(args.console_type)          if args.action == 'rename':              rename_image(args.image_name, args.image_new_name)          if args.action == 'list':              list_images() +        if args.action == 'list_console_types': +            list_console_types()          exit() diff --git a/src/op_mode/ipoe-control.py b/src/op_mode/ipoe-control.py index 0f33beca7..b7d6a0c43 100755 --- a/src/op_mode/ipoe-control.py +++ b/src/op_mode/ipoe-control.py @@ -56,7 +56,11 @@ def main():          if args.selector in cmd_dict['selector'] and args.target:              run(cmd_dict['cmd_base'] + "{0} {1} {2}".format(args.action, args.selector, args.target))          else: -            output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action], decode='utf-8') +            if args.action == "show_sessions": +                ses_pattern = " ifname,username,calling-sid,ip,ip6,ip6-dp,rate-limit,type,comp,state,uptime" +            else: +                ses_pattern = "" +            output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action] + ses_pattern, decode='utf-8')              if not err:                  print(output)              else: 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/ntp.py b/src/op_mode/ntp.py new file mode 100644 index 000000000..e14cc46d0 --- /dev/null +++ b/src/op_mode/ntp.py @@ -0,0 +1,164 @@ +#!/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 csv +import sys +from itertools import chain + +import vyos.opmode +from vyos.configquery import ConfigTreeQuery +from vyos.utils.process import cmd + +def _get_raw_data(command: str) -> dict: +    # Returns returns chronyc output as a dictionary + +    # Initialize dictionary keys to align with output of +    # chrony -c. From some commands, its -c switch outputs +    # more parameters, make sure to include them all below. +    # See to chronyc(1) for definition of key variables +    match command: +        case "chronyc -c activity": +            keys: list = [ +            'sources_online', +            'sources_offline', +            'sources_doing_burst_return_online', +            'sources_doing_burst_return_offline', +            'sources_with_unknown_address' +            ] + +        case "chronyc -c sources": +            keys: list = [ +            'm', +            's', +            'name_ip_address', +            'stratum', +            'poll', +            'reach', +            'last_rx', +            'last_sample_adj_offset', +            'last_sample_mes_offset', +            'last_sample_est_error' +            ] + +        case "chronyc -c sourcestats": +            keys: list = [ +            'name_ip_address', +            'np', +            'nr', +            'span', +            'frequency', +            'freq_skew', +            'offset', +            'std_dev' +            ] + +        case "chronyc -c tracking": +            keys: list = [ +            'ref_id', +            'ref_id_name', +            'stratum', +            'ref_time', +            'system_time', +            'last_offset', +            'rms_offset', +            'frequency', +            'residual_freq', +            'skew', +            'root_delay', +            'root_dispersion', +            'update_interval', +            'leap_status' +            ] + +        case _: +            raise ValueError(f"Raw mode: of {command} is not implemented") + +    # Get -c option command line output, splitlines, +    # and save comma-separated values as a flat list +    output = cmd(command).splitlines() +    values = csv.reader(output) +    values = list(chain.from_iterable(values)) + +    # Divide values into chunks of size keys and transpose +    if len(values) > len(keys): +       values = _chunk_list(values,keys) +       values = zip(*values) + +    return dict(zip(keys, values)) + +def _chunk_list(in_list, n): +    # Yields successive n-sized chunks from in_list +    for i in range(0, len(in_list), len(n)): +        yield in_list[i:i + len(n)] + +def _is_configured(): +    # Check if ntp is configured +    config = ConfigTreeQuery() +    if not config.exists("service ntp"): +        raise vyos.opmode.UnconfiguredSubsystem("NTP service is not enabled.") + +def show_activity(raw: bool): +    _is_configured() +    command = f'chronyc' + +    if raw: +       command += f" -c activity" +       return _get_raw_data(command) +    else: +       command += f" activity" +       return cmd(command) + +def show_sources(raw: bool): +    _is_configured() +    command = f'chronyc' + +    if raw: +       command += f" -c sources" +       return _get_raw_data(command) +    else: +       command += f" sources -v" +       return cmd(command) + +def show_tracking(raw: bool): +    _is_configured() +    command = f'chronyc' + +    if raw: +       command += f" -c tracking" +       return _get_raw_data(command) +    else: +       command += f" tracking" +       return cmd(command) + +def show_sourcestats(raw: bool): +    _is_configured() +    command = f'chronyc' + +    if raw: +       command += f" -c sourcestats" +       return _get_raw_data(command) +    else: +       command += f" sourcestats -v" +       return cmd(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/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}} diff --git a/src/systemd/miniupnpd.service b/src/systemd/miniupnpd.service deleted file mode 100644 index 51cb2eed8..000000000 --- a/src/systemd/miniupnpd.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=UPnP service -ConditionPathExists=/run/upnp/miniupnp.conf -After=vyos-router.service -StartLimitIntervalSec=0 - -[Service] -WorkingDirectory=/run/upnp -Type=simple -ExecStart=/usr/sbin/miniupnpd -d -f /run/upnp/miniupnp.conf -PrivateTmp=yes -PIDFile=/run/miniupnpd.pid -Restart=on-failure  | 
