diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..9623a78 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Reformat YAML: https://github.com/ansible-collections/community.routeros/pull/369 +08152376de116e7d933d19ee25318f7a2eb222ae diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f71b322 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + ci: + patterns: + - "*" diff --git a/.github/patchback.yml b/.github/patchback.yml new file mode 100644 index 0000000..5ee7812 --- /dev/null +++ b/.github/patchback.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +backport_branch_prefix: patchback/backports/ +backport_label_prefix: backport- +target_branch_prefix: stable- +... diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml deleted file mode 100644 index ca6264c..0000000 --- a/.github/workflows/ansible-test.yml +++ /dev/null @@ -1,186 +0,0 @@ -name: CI -on: - # Run CI against all pushes (direct commits, also merged PRs), Pull Requests - push: - pull_request: - # Run CI once per day (at 06:00 UTC) - schedule: - - cron: '0 6 * * *' - -jobs: - -### -# Sanity tests (REQUIRED) -# -# https://docs.ansible.com/ansible/latest/dev_guide/testing_sanity.html - - sanity: - name: Sanity (Ⓐ${{ matrix.ansible }}) - strategy: - matrix: - ansible: - # It's important that Sanity is tested against all stable-X.Y branches - # Testing against `devel` may fail as new tests are added. - - stable-2.9 - - stable-2.10 - - stable-2.11 - - devel - runs-on: ubuntu-latest - steps: - - # ansible-test requires the collection to be in a directory in the form - # .../ansible_collections/community/routeros/ - - - name: Check out code - uses: actions/checkout@v2 - with: - path: ansible_collections/community/routeros - - - name: Set up Python - uses: actions/setup-python@v2 - with: - # it is just required to run that once as "ansible-test sanity" in the docker image - # will run on all python versions it supports. - python-version: 3.8 - - # Install the head of the given branch (devel, stable-2.10) - - name: Install ansible-base (${{ matrix.ansible }}) - run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check - - - name: Install collection dependencies - run: git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.netcommon.git ansible_collections/ansible/netcommon - # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) - # run: ansible-galaxy collection install ansible.netcommon -p . - - # run ansible-test sanity inside of Docker. - # The docker container has all the pinned dependencies that are required - # and all python versions ansible supports. - - name: Run sanity tests - run: ansible-test sanity --docker -v --color - working-directory: ./ansible_collections/community/routeros - -### -# Unit tests (OPTIONAL) -# -# https://docs.ansible.com/ansible/latest/dev_guide/testing_units.html - - units: - runs-on: ubuntu-latest - name: Units (Ⓐ${{ matrix.ansible }}) - strategy: - # As soon as the first unit test fails, cancel the others to free up the CI queue - fail-fast: true - matrix: - ansible: - - stable-2.9 - - stable-2.10 - - stable-2.11 - - devel - - steps: - - name: Check out code - uses: actions/checkout@v2 - with: - path: ansible_collections/community/routeros - - - name: Set up Python ${{ matrix.ansible }} - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install ansible-base (${{ matrix.ansible }}) - run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check - - - name: Install collection dependencies - run: git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.netcommon.git ansible_collections/ansible/netcommon - # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) - # run: ansible-galaxy collection install ansible.netcommon -p . - - # Run the unit tests - - name: Run unit tests for all Python versions - run: ansible-test units -v --color --docker --coverage - working-directory: ./ansible_collections/community/routeros - - # ansible-test support producing code coverage date - - name: Generate coverage report - run: ansible-test coverage xml -v --requirements --group-by command --group-by version - working-directory: ./ansible_collections/community/routeros - - # See the reports at https://codecov.io/gh/ansible_collections/ansible-collections/community.routeros - - uses: codecov/codecov-action@v1 - with: - fail_ci_if_error: false - -### -# Integration tests (RECOMMENDED) -# -# https://docs.ansible.com/ansible/latest/dev_guide/testing_integration.html - - -# If the application you are testing is available as a docker container and you want to test -# multiple versions see the following for an example: -# https://github.com/ansible-collections/community.zabbix/tree/master/.github/workflows - -# integration: -# runs-on: ubuntu-latest -# name: I (Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}}) -# strategy: -# fail-fast: false -# matrix: -# ansible: -# - stable-2.9 -# - stable-2.10 -# - stable-2.11 -# - devel -# python: -# - 2.6 -# - 2.7 -# - 3.5 -# - 3.6 -# - 3.7 -# - 3.8 -# - 3.9 -# - "3.10" -# exclude: -# - ansible: stable-2.9 -# python: 3.9 -# - ansible: stable-2.9 -# python: "3.10" -# - ansible: stable-2.10 -# python: "3.10" -# - ansible: stable-2.11 -# python: "3.10" -# -# steps: -# - name: Check out code -# uses: actions/checkout@v2 -# with: -# path: ansible_collections/community/routeros -# -# - name: Set up Python ${{ matrix.ansible }} -# uses: actions/setup-python@v2 -# with: -# python-version: 3.8 -# -# - name: Install ansible-base (${{ matrix.ansible }}) -# run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check -# -# - name: Install collection dependencies -# run: git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.netcommon.git ansible_collections/ansible/netcommon -# # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) -# # run: ansible-galaxy collection install ansible.netcommon -p . -# -# # Run the integration tests -# - name: Run integration test -# run: ansible-test integration -v --color --retry-on-error --continue-on-error --diff --python ${{ matrix.python }} --docker --coverage -# working-directory: ./ansible_collections/community/routeros -# -# # ansible-test support producing code coverage date -# - name: Generate coverage report -# run: ansible-test coverage xml -v --requirements --group-by command --group-by version -# working-directory: ./ansible_collections/community/routeros -# -# # See the reports at https://codecov.io/gh/ansible_collections/ansible-collections/community.routeros -# - uses: codecov/codecov-action@v1 -# with: -# fail_ci_if_error: false diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml new file mode 100644 index 0000000..63135a1 --- /dev/null +++ b/.github/workflows/docs-pr.yml @@ -0,0 +1,97 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Collection Docs +concurrency: + group: docs-pr-${{ github.head_ref }} + cancel-in-progress: true +'on': + pull_request_target: + types: [opened, synchronize, reopened, closed] + +env: + GHP_BASE_URL: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }} + +jobs: + build-docs: + permissions: + contents: read + name: Build Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-pr.yml@main + with: + collection-name: community.routeros + init-lenient: false + init-fail-on-error: true + squash-hierarchy: true + init-project: Community.Routeros Collection + init-copyright: Community.Routeros Contributors + init-title: Community.Routeros Collection Documentation + init-html-short-title: Community.Routeros Collection Docs + init-extra-html-theme-options: | + documentation_home_url=https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/branch/main/ + render-file-line: '> * `$` [$](https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/pr/${{ github.event.number }}/$)' + provide-link-targets: | + ansible_collections.ansible.netcommon.network_cli_connection__parameter-ssh_type + + publish-docs-gh-pages: + # for now we won't run this on forks + if: github.repository == 'ansible-collections/community.routeros' + permissions: + contents: write + pages: write + id-token: write + needs: [build-docs] + name: Publish Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main + with: + artifact-name: ${{ needs.build-docs.outputs.artifact-name }} + action: ${{ (github.event.action == 'closed' || needs.build-docs.outputs.changed != 'true') && 'teardown' || 'publish' }} + publish-gh-pages-branch: true + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + comment: + permissions: + pull-requests: write + runs-on: ubuntu-latest + needs: [build-docs, publish-docs-gh-pages] + name: PR comments + steps: + - name: PR comment + uses: ansible-community/github-docs-build/actions/ansible-docs-build-comment@main + with: + body-includes: '## Docs Build' + reactions: heart + action: ${{ needs.build-docs.outputs.changed != 'true' && 'remove' || '' }} + on-closed-body: | + ## Docs Build 📝 + + This PR is closed and any previously published docsite has been unpublished. + on-merged-body: | + ## Docs Build 📝 + + Thank you for contribution!✨ + + This PR has been merged and the docs are now incorporated into `main`: + ${{ env.GHP_BASE_URL }}/branch/main + body: | + ## Docs Build 📝 + + Thank you for contribution!✨ + + The docs for **this PR** have been published here: + ${{ env.GHP_BASE_URL }}/pr/${{ github.event.number }} + + You can compare to the docs for the `main` branch here: + ${{ env.GHP_BASE_URL }}/branch/main + + The docsite for **this PR** is also available for download as an artifact from this run: + ${{ needs.build-docs.outputs.artifact-url }} + + File changes: + + ${{ needs.build-docs.outputs.diff-files-rendered }} + + ${{ needs.build-docs.outputs.diff-rendered }} diff --git a/.github/workflows/docs-push.yml b/.github/workflows/docs-push.yml new file mode 100644 index 0000000..3d2c2b1 --- /dev/null +++ b/.github/workflows/docs-push.yml @@ -0,0 +1,55 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Collection Docs +concurrency: + group: docs-push-${{ github.sha }} + cancel-in-progress: true +'on': + push: + branches: + - main + - stable-* + tags: + - '*' + # Run CI once per day (at 05:15 UTC) + schedule: + - cron: '15 5 * * *' + # Allow manual trigger (for newer antsibull-docs, sphinx-ansible-theme, ... versions) + workflow_dispatch: + +jobs: + build-docs: + permissions: + contents: read + name: Build Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-push.yml@main + with: + collection-name: community.routeros + init-lenient: true + init-fail-on-error: true + squash-hierarchy: true + init-project: Community.Routeros Collection + init-copyright: Community.Routeros Contributors + init-title: Community.Routeros Collection Documentation + init-html-short-title: Community.Routeros Collection Docs + init-extra-html-theme-options: | + documentation_home_url=https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/branch/main/ + + publish-docs-gh-pages: + # for now we won't run this on forks + if: github.repository == 'ansible-collections/community.routeros' + permissions: + contents: write + pages: write + id-token: write + needs: [build-docs] + name: Publish Ansible Docs + uses: ansible-community/github-docs-build/.github/workflows/_shared-docs-build-publish-gh-pages.yml@main + with: + artifact-name: ${{ needs.build-docs.outputs.artifact-name }} + publish-gh-pages-branch: true + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/extra-tests.yml b/.github/workflows/extra-tests.yml deleted file mode 100644 index e04bf11..0000000 --- a/.github/workflows/extra-tests.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: extra-tests -on: - # Run CI against all pushes (direct commits, also merged PRs), Pull Requests - push: - pull_request: - # Run CI once per day (at 06:00 UTC) - # This ensures that even if there haven't been commits that we are still testing against latest version of ansible-test for each ansible-base version - schedule: - - cron: '0 6 * * *' -env: - NAMESPACE: community - COLLECTION_NAME: routeros - -jobs: - extra-sanity: - name: Extra Sanity - runs-on: ubuntu-latest - steps: - - - name: Check out code - uses: actions/checkout@v2 - with: - path: ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install ansible-core - run: pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check - - - name: Install collection dependencies - run: git clone --depth=1 --single-branch https://github.com/ansible-collections/community.internal_test_tools.git ./ansible_collections/community/internal_test_tools - # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) - # run: ansible-galaxy collection install community.internal_test_tools -p . - - - name: Run sanity tests - run: ../../community/internal_test_tools/tools/run.py --color - working-directory: ./ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} diff --git a/.github/workflows/import-galaxy.yml b/.github/workflows/import-galaxy.yml deleted file mode 100644 index bb2e488..0000000 --- a/.github/workflows/import-galaxy.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: import-galaxy -on: - # Run CI against all pushes (direct commits, also merged PRs) to main, and all Pull Requests - push: - branches: - - main - pull_request: - -env: - # Adjust this to your collection - NAMESPACE: community - COLLECTION_NAME: routeros - -jobs: - build-collection: - name: Build collection artifact - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v2 - with: - path: ./checkout - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install ansible-core - run: pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check - - - name: Make sure galaxy.yml has version entry - run: >- - python -c - 'import yaml ; - f = open("galaxy.yml", "rb") ; - data = yaml.safe_load(f) ; - f.close() ; - data["version"] = data.get("version") or "0.0.1" ; - f = open("galaxy.yml", "wb") ; - f.write(yaml.dump(data).encode("utf-8")) ; - f.close() ; - ' - working-directory: ./checkout - - - name: Build collection - run: ansible-galaxy collection build - working-directory: ./checkout - - - name: Copy artifact into subdirectory - run: mkdir ./artifact && mv ./checkout/${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}-*.tar.gz ./artifact - - - name: Upload artifact - uses: actions/upload-artifact@v2 - with: - name: ${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}-${{ github.sha }} - path: ./artifact/ - - import-galaxy: - name: Import artifact with Galaxy importer - runs-on: ubuntu-latest - needs: - - build-collection - steps: - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install ansible-core - run: pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check - - - name: Install galaxy-importer - run: pip install galaxy-importer --disable-pip-version-check - - - name: Download artifact - uses: actions/download-artifact@v2 - with: - name: ${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}-${{ github.sha }} - - - name: Run Galaxy importer - run: python -m galaxy_importer.main ${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}-*.tar.gz diff --git a/.github/workflows/nox.yml b/.github/workflows/nox.yml new file mode 100644 index 0000000..a539157 --- /dev/null +++ b/.github/workflows/nox.yml @@ -0,0 +1,35 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +name: nox +'on': + push: + branches: + - main + - stable-* + pull_request: + # Run CI once per day (at 05:15 UTC) + schedule: + - cron: '15 5 * * *' + workflow_dispatch: + +jobs: + nox: + runs-on: ubuntu-latest + name: "Run extra sanity tests" + steps: + - name: Check out collection + uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Run nox + uses: ansible-community/antsibull-nox@main + + ansible-test: + uses: ansible-community/antsibull-nox/.github/workflows/reusable-nox-matrix.yml@main + with: + upload-codecov: true + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 740c811..728531b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + /tests/output/ /changelogs/.plugin-cache.yaml +/tests/integration/inventory # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..a6707e2 --- /dev/null +++ b/.yamllint @@ -0,0 +1,53 @@ +--- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: 2025 Felix Fontein + +extends: default + +ignore: | + /changelogs/ + +rules: + line-length: + max: 300 + level: error + document-start: + present: true + document-end: false + truthy: + level: error + allowed-values: + - 'true' + - 'false' + indentation: + spaces: 2 + indent-sequences: true + key-duplicates: enable + trailing-spaces: enable + new-line-at-end-of-file: disable + hyphens: + max-spaces-after: 1 + empty-lines: + max: 2 + max-start: 0 + max-end: 0 + commas: + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + colons: + max-spaces-before: 0 + max-spaces-after: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true + comments: + min-spaces-from-content: 1 + comments-indentation: false diff --git a/.yamllint-docs b/.yamllint-docs new file mode 100644 index 0000000..de8947d --- /dev/null +++ b/.yamllint-docs @@ -0,0 +1,54 @@ +--- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: 2025 Felix Fontein + +extends: default + +ignore: | + /changelogs/ + +rules: + line-length: + max: 160 + level: error + document-start: + present: false + document-end: + present: false + truthy: + level: error + allowed-values: + - 'true' + - 'false' + indentation: + spaces: 2 + indent-sequences: true + key-duplicates: enable + trailing-spaces: enable + new-line-at-end-of-file: disable + hyphens: + max-spaces-after: 1 + empty-lines: + max: 2 + max-start: 0 + max-end: 0 + commas: + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + colons: + max-spaces-before: 0 + max-spaces-after: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true + comments: + min-spaces-from-content: 1 + comments-indentation: false diff --git a/.yamllint-examples b/.yamllint-examples new file mode 100644 index 0000000..062ac5a --- /dev/null +++ b/.yamllint-examples @@ -0,0 +1,54 @@ +--- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: 2025 Felix Fontein + +extends: default + +ignore: | + /changelogs/ + +rules: + line-length: + max: 160 + level: error + document-start: + present: true + document-end: + present: false + truthy: + level: error + allowed-values: + - 'true' + - 'false' + indentation: + spaces: 2 + indent-sequences: true + key-duplicates: enable + trailing-spaces: enable + new-line-at-end-of-file: disable + hyphens: + max-spaces-after: 1 + empty-lines: + max: 2 + max-start: 0 + max-end: 0 + commas: + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + colons: + max-spaces-before: 0 + max-spaces-after: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true + comments: + min-spaces-from-content: 1 + comments-indentation: false diff --git a/.yamllint-extra-docs b/.yamllint-extra-docs new file mode 100644 index 0000000..7e24c0f --- /dev/null +++ b/.yamllint-extra-docs @@ -0,0 +1,53 @@ +--- +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: 2025 Felix Fontein + +extends: default + +ignore: | + /changelogs/ + +rules: + line-length: + max: 160 + level: error + document-start: disable + document-end: + present: false + truthy: + level: error + allowed-values: + - 'true' + - 'false' + indentation: + spaces: 2 + indent-sequences: true + key-duplicates: enable + trailing-spaces: enable + new-line-at-end-of-file: disable + hyphens: + max-spaces-after: 1 + empty-lines: + max: 2 + max-start: 0 + max-end: 0 + commas: + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + colons: + max-spaces-before: 0 + max-spaces-after: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true + comments: + min-spaces-from-content: 1 + comments-indentation: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..af8c632 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,977 @@ +# Community RouterOS Release Notes + +**Topics** + +- v3\.9\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v3\.8\.1 + - Release Summary + - Bugfixes +- v3\.8\.0 + - Release Summary + - Minor Changes +- v3\.7\.0 + - Release Summary + - Minor Changes +- v3\.6\.0 + - Release Summary + - Minor Changes +- v3\.5\.0 + - Release Summary + - Minor Changes +- v3\.4\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v3\.3\.0 + - Release Summary + - Minor Changes +- v3\.2\.0 + - Release Summary + - Minor Changes +- v3\.1\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v3\.0\.0 + - Release Summary + - Breaking Changes / Porting Guide + - Removed Features \(previously deprecated\) +- v2\.20\.0 + - Release Summary + - Minor Changes +- v2\.19\.0 + - Release Summary + - Minor Changes +- v2\.18\.0 + - Release Summary + - Minor Changes + - Deprecated Features + - Bugfixes +- v2\.17\.0 + - Release Summary + - Minor Changes +- v2\.16\.0 + - Release Summary + - Minor Changes +- v2\.15\.0 + - Release Summary + - Minor Changes +- v2\.14\.0 + - Release Summary + - Minor Changes +- v2\.13\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v2\.12\.0 + - Release Summary + - Minor Changes +- v2\.11\.0 + - Release Summary + - Minor Changes +- v2\.10\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v2\.9\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v2\.8\.3 + - Release Summary + - Known Issues +- v2\.8\.2 + - Release Summary + - Bugfixes +- v2\.8\.1 + - Release Summary + - Bugfixes +- v2\.8\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v2\.7\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v2\.6\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v2\.5\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v2\.4\.0 + - Release Summary + - Minor Changes + - Bugfixes + - Known Issues +- v2\.3\.1 + - Release Summary + - Known Issues +- v2\.3\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v2\.2\.1 + - Release Summary + - Bugfixes +- v2\.2\.0 + - Release Summary + - Minor Changes + - Bugfixes + - New Modules +- v2\.1\.0 + - Release Summary + - Minor Changes + - Bugfixes + - New Modules +- v2\.0\.0 + - Release Summary + - Minor Changes + - Breaking Changes / Porting Guide + - Bugfixes + - New Plugins + - Filter +- v1\.2\.0 + - Release Summary + - Minor Changes + - Bugfixes +- v1\.1\.0 + - Release Summary + - Minor Changes +- v1\.0\.1 + - Release Summary + - Bugfixes +- v1\.0\.0 + - Release Summary + - Bugfixes +- v0\.1\.1 + - Release Summary + - Bugfixes +- v0\.1\.0 + - Release Summary + - Minor Changes + + +## v3\.9\.0 + + +### Release Summary + +Bugfix and feature release\. + + +### Minor Changes + +* api\_info\, api modify \- add remote\-log\-format\, remote\-protocol\, and event\-delimiter to system logging action \([https\://github\.com/ansible\-collections/community\.routeros/pull/381](https\://github\.com/ansible\-collections/community\.routeros/pull/381)\)\. +* api\_info\, api\_modify \- add disable\-link\-local\-address and stale\-neighbor\-timeout fields to ipv6 settings \([https\://github\.com/ansible\-collections/community\.routeros/pull/380](https\://github\.com/ansible\-collections/community\.routeros/pull/380)\)\. +* api\_info\, api\_modify \- adjust neighbor limit fields in ipv6 settings to match RouterOS 7\.18 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/380](https\://github\.com/ansible\-collections/community\.routeros/pull/380)\)\. +* api\_info\, api\_modify \- set passthrough default in ip firewall mangle to true for RouterOS 7\.19 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/382](https\://github\.com/ansible\-collections/community\.routeros/pull/382)\)\. +* api\_info\, api\_modify \- since RouterOS 7\.17 VRF is supported for OVPN server\. It now supports multiple entries\, while api\_modify so far only accepted a single entry\. The interface ovpn\-server server path now allows multiple entries on RouterOS 7\.17 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/383](https\://github\.com/ansible\-collections/community\.routeros/pull/383)\)\. + + +### Bugfixes + +* routeros terminal plugin \- fix terminal\_stdout\_re pattern to handle long system identities when connecting to RouterOS through SSH \([https\://github\.com/ansible\-collections/community\.routeros/pull/386](https\://github\.com/ansible\-collections/community\.routeros/pull/386)\)\. + + +## v3\.8\.1 + + +### Release Summary + +Bugfix release\. + + +### Bugfixes + +* facts and api\_facts modules \- prevent deprecation warnings when used with ansible\-core 2\.19 \([https\://github\.com/ansible\-collections/community\.routeros/pull/384](https\://github\.com/ansible\-collections/community\.routeros/pull/384)\)\. + + +## v3\.8\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add interface ethernet switch port\-isolation which is supported since RouterOS 6\.43 \([https\://github\.com/ansible\-collections/community\.routeros/pull/375](https\://github\.com/ansible\-collections/community\.routeros/pull/375)\)\. +* api\_info\, api\_modify \- add routing bfd configuration\. Officially stabilized BFD support for BGP and OSPF is available since RouterOS 7\.11 + \([https\://github\.com/ansible\-collections/community\.routeros/pull/375](https\://github\.com/ansible\-collections/community\.routeros/pull/375)\)\. +* api\_modify\, api\_info \- support API path ip ipsec mode\-config \([https\://github\.com/ansible\-collections/community\.routeros/pull/376](https\://github\.com/ansible\-collections/community\.routeros/pull/376)\)\. + + +## v3\.7\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_find\_and\_modify \- allow to control whether dynamic and/or builtin entries are ignored with the new ignore\_dynamic and ignore\_builtin options \([https\://github\.com/ansible\-collections/community\.routeros/issues/372](https\://github\.com/ansible\-collections/community\.routeros/issues/372)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/373](https\://github\.com/ansible\-collections/community\.routeros/pull/373)\)\. +* api\_info\, api\_modify \- add port\-cost\-mode to interface bridge which is supported since RouterOS 7\.13 \([https\://github\.com/ansible\-collections/community\.routeros/pull/371](https\://github\.com/ansible\-collections/community\.routeros/pull/371)\)\. + + +## v3\.6\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add mdns\-repeat\-ifaces to ip dns for RouterOS 7\.16 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/358](https\://github\.com/ansible\-collections/community\.routeros/pull/358)\)\. +* api\_info\, api\_modify \- field name change in routing bgp connection path implemented by RouterOS 7\.19 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/360](https\://github\.com/ansible\-collections/community\.routeros/pull/360)\)\. +* api\_info\, api\_modify \- rename is\-responder property in interface wireguard peers to responder for RouterOS 7\.17 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/364](https\://github\.com/ansible\-collections/community\.routeros/pull/364)\)\. + + +## v3\.5\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- change default for /ip/cloud/ddns\-enabled for RouterOS 7\.17 and newer from yes to auto \([https\://github\.com/ansible\-collections/community\.routeros/pull/350](https\://github\.com/ansible\-collections/community\.routeros/pull/350)\)\. + + +## v3\.4\.0 + + +### Release Summary + +Feature and bugfix release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add support for the ip dns forwarders path implemented by RouterOS 7\.17 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/343](https\://github\.com/ansible\-collections/community\.routeros/pull/343)\)\. + + +### Bugfixes + +* api\_info\, api\_modify \- remove the primary key action from the interface wifi provisioning path\, since RouterOS also allows to create completely duplicate entries \([https\://github\.com/ansible\-collections/community\.routeros/issues/344](https\://github\.com/ansible\-collections/community\.routeros/issues/344)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/345](https\://github\.com/ansible\-collections/community\.routeros/pull/345)\)\. + + +## v3\.3\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add missing attribute require\-message\-auth for the radius path which exists since RouterOS version 7\.15 \([https\://github\.com/ansible\-collections/community\.routeros/issues/338](https\://github\.com/ansible\-collections/community\.routeros/issues/338)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/339](https\://github\.com/ansible\-collections/community\.routeros/pull/339)\)\. +* api\_info\, api\_modify \- add the interface 6to4 path\. Used to manage IPv6 tunnels via tunnel\-brokers like HE\, where native IPv6 is not provided \([https\://github\.com/ansible\-collections/community\.routeros/pull/342](https\://github\.com/ansible\-collections/community\.routeros/pull/342)\)\. +* api\_info\, api\_modify \- add the interface wireless access\-list and interface wireless connect\-list paths \([https\://github\.com/ansible\-collections/community\.routeros/issues/284](https\://github\.com/ansible\-collections/community\.routeros/issues/284)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/340](https\://github\.com/ansible\-collections/community\.routeros/pull/340)\)\. +* api\_info\, api\_modify \- add the use\-interface\-duid option for ipv6 dhcp\-client path\. This option prevents issues with Fritzbox modems and routers\, when using virtual interfaces \(like VLANs\) may create duplicated records in hosts config\, this breaks original \"expose\-host\" function\. Also add the script\, custom\-duid and validate\-server\-duid as backport from 7\.15 version update \([https\://github\.com/ansible\-collections/community\.routeros/pull/341](https\://github\.com/ansible\-collections/community\.routeros/pull/341)\)\. + + +## v3\.2\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add support for the routing filter community\-list path implemented by RouterOS 7 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/331](https\://github\.com/ansible\-collections/community\.routeros/pull/331)\)\. + + +## v3\.1\.0 + + +### Release Summary + +Bugfix and feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add missing fields comment\, next\-pool to ip pool path \([https\://github\.com/ansible\-collections/community\.routeros/pull/327](https\://github\.com/ansible\-collections/community\.routeros/pull/327)\)\. + + +### Bugfixes + +* api\_info\, api\_modify \- fields log and log\-prefix in paths ip firewall filter\, ip firewall mangle\, ip firewall nat\, ip firewall raw now have the correct default values \([https\://github\.com/ansible\-collections/community\.routeros/pull/324](https\://github\.com/ansible\-collections/community\.routeros/pull/324)\)\. + + +## v3\.0\.0 + + +### Release Summary + +Major release that drops support for End of Life Python versions and fixes check mode for community\.routeros\.command\. + + +### Breaking Changes / Porting Guide + +* command \- the module no longer declares that it supports check mode \([https\://github\.com/ansible\-collections/community\.routeros/pull/318](https\://github\.com/ansible\-collections/community\.routeros/pull/318)\)\. + + +### Removed Features \(previously deprecated\) + +* The collection no longer supports Ansible 2\.9\, ansible\-base 2\.10\, ansible\-core 2\.11\, ansible\-core 2\.12\, ansible\-core 2\.13\, and ansible\-core 2\.14\. If you need to continue using End of Life versions of Ansible/ansible\-base/ansible\-core\, please use community\.routeros 2\.x\.y \([https\://github\.com/ansible\-collections/community\.routeros/pull/318](https\://github\.com/ansible\-collections/community\.routeros/pull/318)\)\. + + +## v2\.20\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add new parameters from the RouterOS 7\.16 release \([https\://github\.com/ansible\-collections/community\.routeros/pull/323](https\://github\.com/ansible\-collections/community\.routeros/pull/323)\)\. +* api\_info\, api\_modify \- add support interface l2tp\-client configuration \([https\://github\.com/ansible\-collections/community\.routeros/pull/322](https\://github\.com/ansible\-collections/community\.routeros/pull/322)\)\. +* api\_info\, api\_modify \- add support for the cpu\-frequency\, memory\-frequency\, preboot\-etherboot and preboot\-etherboot\-server properties in system routerboard settings \([https\://github\.com/ansible\-collections/community\.routeros/pull/320](https\://github\.com/ansible\-collections/community\.routeros/pull/320)\)\. +* api\_info\, api\_modify \- add support for the matching\-type property in ip dhcp\-server matcher introduced by RouterOS 7\.16 \([https\://github\.com/ansible\-collections/community\.routeros/pull/321](https\://github\.com/ansible\-collections/community\.routeros/pull/321)\)\. + + +## v2\.19\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add support for the ip dns adlist path implemented by RouterOS 7\.15 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/310](https\://github\.com/ansible\-collections/community\.routeros/pull/310)\)\. +* api\_info\, api\_modify \- add support for the mld\-version and multicast\-querier properties in interface bridge \([https\://github\.com/ansible\-collections/community\.routeros/pull/315](https\://github\.com/ansible\-collections/community\.routeros/pull/315)\)\. +* api\_info\, api\_modify \- add support for the routing filter num\-list path implemented by RouterOS 7 and newer \([https\://github\.com/ansible\-collections/community\.routeros/pull/313](https\://github\.com/ansible\-collections/community\.routeros/pull/313)\)\. +* api\_info\, api\_modify \- add support for the routing igmp\-proxy path \([https\://github\.com/ansible\-collections/community\.routeros/pull/309](https\://github\.com/ansible\-collections/community\.routeros/pull/309)\)\. +* api\_modify\, api\_info \- add read\-only default field to snmp community \([https\://github\.com/ansible\-collections/community\.routeros/pull/311](https\://github\.com/ansible\-collections/community\.routeros/pull/311)\)\. + + +## v2\.18\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info \- allow to restrict the output by limiting fields to specific values with the new restrict option \([https\://github\.com/ansible\-collections/community\.routeros/pull/305](https\://github\.com/ansible\-collections/community\.routeros/pull/305)\)\. +* api\_info\, api\_modify \- add support for the ip dhcp\-server matcher path \([https\://github\.com/ansible\-collections/community\.routeros/pull/300](https\://github\.com/ansible\-collections/community\.routeros/pull/300)\)\. +* api\_info\, api\_modify \- add support for the ipv6 nd prefix path \([https\://github\.com/ansible\-collections/community\.routeros/pull/303](https\://github\.com/ansible\-collections/community\.routeros/pull/303)\)\. +* api\_info\, api\_modify \- add support for the name and is\-responder properties under the interface wireguard peers path introduced in RouterOS 7\.15 \([https\://github\.com/ansible\-collections/community\.routeros/pull/304](https\://github\.com/ansible\-collections/community\.routeros/pull/304)\)\. +* api\_info\, api\_modify \- add support for the routing ospf static\-neighbor path in RouterOS 7 \([https\://github\.com/ansible\-collections/community\.routeros/pull/302](https\://github\.com/ansible\-collections/community\.routeros/pull/302)\)\. +* api\_info\, api\_modify \- set default for force in ip dhcp\-server option to an explicit false \([https\://github\.com/ansible\-collections/community\.routeros/pull/300](https\://github\.com/ansible\-collections/community\.routeros/pull/300)\)\. +* api\_modify \- allow to restrict what is updated by limiting fields to specific values with the new restrict option \([https\://github\.com/ansible\-collections/community\.routeros/pull/305](https\://github\.com/ansible\-collections/community\.routeros/pull/305)\)\. + + +### Deprecated Features + +* The collection deprecates support for all Ansible/ansible\-base/ansible\-core versions that are currently End of Life\, [according to the ansible\-core support matrix](https\://docs\.ansible\.com/ansible\-core/devel/reference\_appendices/release\_and\_maintenance\.html\#ansible\-core\-support\-matrix)\. This means that the next major release of the collection will no longer support Ansible 2\.9\, ansible\-base 2\.10\, ansible\-core 2\.11\, ansible\-core 2\.12\, ansible\-core 2\.13\, and ansible\-core 2\.14\. + + +### Bugfixes + +* api\_modify\, api\_info \- change the default of ingress\-filtering in paths interface bridge and interface bridge port back to false for RouterOS before version 7 \([https\://github\.com/ansible\-collections/community\.routeros/pull/305](https\://github\.com/ansible\-collections/community\.routeros/pull/305)\)\. + + +## v2\.17\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add system health settings path \([https\://github\.com/ansible\-collections/community\.routeros/pull/294](https\://github\.com/ansible\-collections/community\.routeros/pull/294)\)\. +* api\_info\, api\_modify \- add missing path /system resource irq rps \([https\://github\.com/ansible\-collections/community\.routeros/pull/295](https\://github\.com/ansible\-collections/community\.routeros/pull/295)\)\. +* api\_info\, api\_modify \- add parameter host\-key\-type for ip ssh path \([https\://github\.com/ansible\-collections/community\.routeros/issues/280](https\://github\.com/ansible\-collections/community\.routeros/issues/280)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/297](https\://github\.com/ansible\-collections/community\.routeros/pull/297)\)\. + + +## v2\.16\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add missing path /ppp secret \([https\://github\.com/ansible\-collections/community\.routeros/pull/286](https\://github\.com/ansible\-collections/community\.routeros/pull/286)\)\. +* api\_info\, api\_modify \- minor changes /interface ethernet path fields \([https\://github\.com/ansible\-collections/community\.routeros/pull/288](https\://github\.com/ansible\-collections/community\.routeros/pull/288)\)\. + + +## v2\.15\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- Add RouterOS 7\.x support to /mpls ldp path \([https\://github\.com/ansible\-collections/community\.routeros/pull/271](https\://github\.com/ansible\-collections/community\.routeros/pull/271)\)\. +* api\_info\, api\_modify \- add /ip route rule path for RouterOS 6\.x \([https\://github\.com/ansible\-collections/community\.routeros/pull/278](https\://github\.com/ansible\-collections/community\.routeros/pull/278)\)\. +* api\_info\, api\_modify \- add /routing filter path for RouterOS 6\.x \([https\://github\.com/ansible\-collections/community\.routeros/pull/279](https\://github\.com/ansible\-collections/community\.routeros/pull/279)\)\. +* api\_info\, api\_modify \- add default value for from\-pool field in /ipv6 address \([https\://github\.com/ansible\-collections/community\.routeros/pull/270](https\://github\.com/ansible\-collections/community\.routeros/pull/270)\)\. +* api\_info\, api\_modify \- add missing path /interface pppoe\-server server \([https\://github\.com/ansible\-collections/community\.routeros/pull/273](https\://github\.com/ansible\-collections/community\.routeros/pull/273)\)\. +* api\_info\, api\_modify \- add missing path /ip dhcp\-relay \([https\://github\.com/ansible\-collections/community\.routeros/pull/276](https\://github\.com/ansible\-collections/community\.routeros/pull/276)\)\. +* api\_info\, api\_modify \- add missing path /queue simple \([https\://github\.com/ansible\-collections/community\.routeros/pull/269](https\://github\.com/ansible\-collections/community\.routeros/pull/269)\)\. +* api\_info\, api\_modify \- add missing path /queue type \([https\://github\.com/ansible\-collections/community\.routeros/pull/274](https\://github\.com/ansible\-collections/community\.routeros/pull/274)\)\. +* api\_info\, api\_modify \- add missing paths /routing bgp aggregate\, /routing bgp network and /routing bgp peer \([https\://github\.com/ansible\-collections/community\.routeros/pull/277](https\://github\.com/ansible\-collections/community\.routeros/pull/277)\)\. +* api\_info\, api\_modify \- add support for paths /mpls interface\, /mpls ldp accept\-filter\, /mpls ldp advertise\-filter and mpls ldp interface \([https\://github\.com/ansible\-collections/community\.routeros/pull/272](https\://github\.com/ansible\-collections/community\.routeros/pull/272)\)\. + + +## v2\.14\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add read\-only fields installed\-version\, latest\-version and status in system package update \([https\://github\.com/ansible\-collections/community\.routeros/pull/263](https\://github\.com/ansible\-collections/community\.routeros/pull/263)\)\. +* api\_info\, api\_modify \- added support for interface wifi and its sub\-paths \([https\://github\.com/ansible\-collections/community\.routeros/pull/266](https\://github\.com/ansible\-collections/community\.routeros/pull/266)\)\. +* api\_info\, api\_modify \- remove default value for read\-only running field in interface wireless \([https\://github\.com/ansible\-collections/community\.routeros/pull/264](https\://github\.com/ansible\-collections/community\.routeros/pull/264)\)\. + + +## v2\.13\.0 + + +### Release Summary + +Bugfix and feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- make path user group modifiable and add comment attribute \([https\://github\.com/ansible\-collections/community\.routeros/issues/256](https\://github\.com/ansible\-collections/community\.routeros/issues/256)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/257](https\://github\.com/ansible\-collections/community\.routeros/pull/257)\)\. +* api\_modify\, api\_info \- add support for the ip vrf path in RouterOS 7 \([https\://github\.com/ansible\-collections/community\.routeros/pull/259](https\://github\.com/ansible\-collections/community\.routeros/pull/259)\) + + +### Bugfixes + +* facts \- fix date not getting removed for idempotent config export \([https\://github\.com/ansible\-collections/community\.routeros/pull/262](https\://github\.com/ansible\-collections/community\.routeros/pull/262)\)\. + + +## v2\.12\.0 + + +### Release Summary + +Feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add interface ovpn\-client path \([https\://github\.com/ansible\-collections/community\.routeros/issues/242](https\://github\.com/ansible\-collections/community\.routeros/issues/242)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/244](https\://github\.com/ansible\-collections/community\.routeros/pull/244)\)\. +* api\_info\, api\_modify \- add radius path \([https\://github\.com/ansible\-collections/community\.routeros/issues/241](https\://github\.com/ansible\-collections/community\.routeros/issues/241)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/245](https\://github\.com/ansible\-collections/community\.routeros/pull/245)\)\. +* api\_info\, api\_modify \- add routing rule path \([https\://github\.com/ansible\-collections/community\.routeros/issues/162](https\://github\.com/ansible\-collections/community\.routeros/issues/162)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/246](https\://github\.com/ansible\-collections/community\.routeros/pull/246)\)\. +* api\_info\, api\_modify \- add missing path routing bgp template \([https\://github\.com/ansible\-collections/community\.routeros/pull/243](https\://github\.com/ansible\-collections/community\.routeros/pull/243)\)\. +* api\_info\, api\_modify \- add support for the tx\-power attribute in interface wireless \([https\://github\.com/ansible\-collections/community\.routeros/pull/239](https\://github\.com/ansible\-collections/community\.routeros/pull/239)\)\. +* api\_info\, api\_modify \- removed host primary key in tool netwatch path \([https\://github\.com/ansible\-collections/community\.routeros/pull/248](https\://github\.com/ansible\-collections/community\.routeros/pull/248)\)\. +* api\_modify\, api\_info \- added support for interface wifiwave2 \([https\://github\.com/ansible\-collections/community\.routeros/pull/226](https\://github\.com/ansible\-collections/community\.routeros/pull/226)\)\. + + +## v2\.11\.0 + + +### Release Summary + +Feature and bugfix release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add missing DoH parameters doh\-max\-concurrent\-queries\, doh\-max\-server\-connections\, and doh\-timeout to the ip dns path \([https\://github\.com/ansible\-collections/community\.routeros/issues/230](https\://github\.com/ansible\-collections/community\.routeros/issues/230)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/235](https\://github\.com/ansible\-collections/community\.routeros/pull/235)\) +* api\_info\, api\_modify \- add missing parameters address\-list\, address\-list\-timeout\, randomise\-ports\, and realm to subpaths of the ip firewall path \([https\://github\.com/ansible\-collections/community\.routeros/issues/236](https\://github\.com/ansible\-collections/community\.routeros/issues/236)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/237](https\://github\.com/ansible\-collections/community\.routeros/pull/237)\)\. +* api\_info\, api\_modify \- mark the interface wireless parameter running as read\-only \([https\://github\.com/ansible\-collections/community\.routeros/pull/233](https\://github\.com/ansible\-collections/community\.routeros/pull/233)\)\. +* api\_info\, api\_modify \- set the default value to false for the disabled parameter in some more paths where it can be seen in the documentation \([https\://github\.com/ansible\-collections/community\.routeros/pull/237](https\://github\.com/ansible\-collections/community\.routeros/pull/237)\)\. +* api\_modify \- add missing comment attribute to /routing id \([https\://github\.com/ansible\-collections/community\.routeros/pull/234](https\://github\.com/ansible\-collections/community\.routeros/pull/234)\)\. +* api\_modify \- add missing attributes to the routing bgp connection path \([https\://github\.com/ansible\-collections/community\.routeros/pull/234](https\://github\.com/ansible\-collections/community\.routeros/pull/234)\)\. +* api\_modify \- add versioning to the /tool e\-mail path \(RouterOS 7\.12 release\) \([https\://github\.com/ansible\-collections/community\.routeros/pull/234](https\://github\.com/ansible\-collections/community\.routeros/pull/234)\)\. +* api\_modify \- make /ip traffic\-flow target a multiple value attribute \([https\://github\.com/ansible\-collections/community\.routeros/pull/234](https\://github\.com/ansible\-collections/community\.routeros/pull/234)\)\. + + +## v2\.10\.0 + + +### Release Summary + +Bugfix and feature release\. + + +### Minor Changes + +* api\_info \- add new include\_read\_only option to select behavior for read\-only values\. By default these are not returned \([https\://github\.com/ansible\-collections/community\.routeros/pull/213](https\://github\.com/ansible\-collections/community\.routeros/pull/213)\)\. +* api\_info\, api\_modify \- add support for address\-list and match\-subdomain introduced by RouterOS 7\.7 in the ip dns static path \([https\://github\.com/ansible\-collections/community\.routeros/pull/197](https\://github\.com/ansible\-collections/community\.routeros/pull/197)\)\. +* api\_info\, api\_modify \- add support for user\, time and gmt\-offset under the system clock path \([https\://github\.com/ansible\-collections/community\.routeros/pull/210](https\://github\.com/ansible\-collections/community\.routeros/pull/210)\)\. +* api\_info\, api\_modify \- add support for the interface ppp\-client path \([https\://github\.com/ansible\-collections/community\.routeros/pull/199](https\://github\.com/ansible\-collections/community\.routeros/pull/199)\)\. +* api\_info\, api\_modify \- add support for the interface wireless path \([https\://github\.com/ansible\-collections/community\.routeros/pull/195](https\://github\.com/ansible\-collections/community\.routeros/pull/195)\)\. +* api\_info\, api\_modify \- add support for the iot modbus path \([https\://github\.com/ansible\-collections/community\.routeros/pull/205](https\://github\.com/ansible\-collections/community\.routeros/pull/205)\)\. +* api\_info\, api\_modify \- add support for the ip dhcp\-server option and ip dhcp\-server option sets paths \([https\://github\.com/ansible\-collections/community\.routeros/pull/223](https\://github\.com/ansible\-collections/community\.routeros/pull/223)\)\. +* api\_info\, api\_modify \- add support for the ip upnp interfaces\, tool graphing interface\, tool graphing resource paths \([https\://github\.com/ansible\-collections/community\.routeros/pull/227](https\://github\.com/ansible\-collections/community\.routeros/pull/227)\)\. +* api\_info\, api\_modify \- add support for the ipv6 firewall nat path \([https\://github\.com/ansible\-collections/community\.routeros/pull/204](https\://github\.com/ansible\-collections/community\.routeros/pull/204)\)\. +* api\_info\, api\_modify \- add support for the mode property in ip neighbor discovery\-settings introduced in RouterOS 7\.7 \([https\://github\.com/ansible\-collections/community\.routeros/pull/198](https\://github\.com/ansible\-collections/community\.routeros/pull/198)\)\. +* api\_info\, api\_modify \- add support for the port remote\-access path \([https\://github\.com/ansible\-collections/community\.routeros/pull/224](https\://github\.com/ansible\-collections/community\.routeros/pull/224)\)\. +* api\_info\, api\_modify \- add support for the routing filter rule and routing filter select\-rule paths \([https\://github\.com/ansible\-collections/community\.routeros/pull/200](https\://github\.com/ansible\-collections/community\.routeros/pull/200)\)\. +* api\_info\, api\_modify \- add support for the routing table path in RouterOS 7 \([https\://github\.com/ansible\-collections/community\.routeros/pull/215](https\://github\.com/ansible\-collections/community\.routeros/pull/215)\)\. +* api\_info\, api\_modify \- add support for the tool netwatch path in RouterOS 7 \([https\://github\.com/ansible\-collections/community\.routeros/pull/216](https\://github\.com/ansible\-collections/community\.routeros/pull/216)\)\. +* api\_info\, api\_modify \- add support for the user settings path \([https\://github\.com/ansible\-collections/community\.routeros/pull/201](https\://github\.com/ansible\-collections/community\.routeros/pull/201)\)\. +* api\_info\, api\_modify \- add support for the user path \([https\://github\.com/ansible\-collections/community\.routeros/pull/211](https\://github\.com/ansible\-collections/community\.routeros/pull/211)\)\. +* api\_info\, api\_modify \- finalize fields for the interface wireless security\-profiles path and enable it \([https\://github\.com/ansible\-collections/community\.routeros/pull/203](https\://github\.com/ansible\-collections/community\.routeros/pull/203)\)\. +* api\_info\, api\_modify \- finalize fields for the ppp profile path and enable it \([https\://github\.com/ansible\-collections/community\.routeros/pull/217](https\://github\.com/ansible\-collections/community\.routeros/pull/217)\)\. +* api\_modify \- add new handle\_read\_only and handle\_write\_only options to handle the module\'s behavior for read\-only and write\-only fields \([https\://github\.com/ansible\-collections/community\.routeros/pull/213](https\://github\.com/ansible\-collections/community\.routeros/pull/213)\)\. +* api\_modify\, api\_info \- support API paths routing id\, routing bgp connection \([https\://github\.com/ansible\-collections/community\.routeros/pull/220](https\://github\.com/ansible\-collections/community\.routeros/pull/220)\)\. + + +### Bugfixes + +* api\_info\, api\_modify \- in the snmp path\, ensure that engine\-id\-suffix is only available on RouterOS 7\.10\+\, and that engine\-id is read\-only on RouterOS 7\.10\+ \([https\://github\.com/ansible\-collections/community\.routeros/issues/208](https\://github\.com/ansible\-collections/community\.routeros/issues/208)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/218](https\://github\.com/ansible\-collections/community\.routeros/pull/218)\)\. + + +## v2\.9\.0 + + +### Release Summary + +Bugfix and feature release\. + + +### Minor Changes + +* api\_info\, api\_modify \- add path caps\-man channel and enable path caps\-man manager interface \([https\://github\.com/ansible\-collections/community\.routeros/issues/193](https\://github\.com/ansible\-collections/community\.routeros/issues/193)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/194](https\://github\.com/ansible\-collections/community\.routeros/pull/194)\)\. +* api\_info\, api\_modify \- add path ip traffic\-flow target \([https\://github\.com/ansible\-collections/community\.routeros/issues/191](https\://github\.com/ansible\-collections/community\.routeros/issues/191)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/192](https\://github\.com/ansible\-collections/community\.routeros/pull/192)\)\. + + +### Bugfixes + +* api\_modify\, api\_info \- add missing parameter engine\-id\-suffix for the snmp path \([https\://github\.com/ansible\-collections/community\.routeros/issues/189](https\://github\.com/ansible\-collections/community\.routeros/issues/189)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/190](https\://github\.com/ansible\-collections/community\.routeros/pull/190)\)\. + + +## v2\.8\.3 + + +### Release Summary + +Maintenance release with updated documentation\. + +From this version on\, community\.routeros is using the new [Ansible semantic markup](https\://docs\.ansible\.com/ansible/devel/dev\_guide/developing\_modules\_documenting\.html\#semantic\-markup\-within\-module\-documentation) +in its documentation\. If you look at documentation with the ansible\-doc CLI tool +from ansible\-core before 2\.15\, please note that it does not render the markup +correctly\. You should be still able to read it in most cases\, but you need +ansible\-core 2\.15 or later to see it as it is intended\. Alternatively you can +look at [the devel docsite](https\://docs\.ansible\.com/ansible/devel/collections/community/routeros/) +for the rendered HTML version of the documentation of the latest release\. + + +### Known Issues + +* Ansible markup will show up in raw form on ansible\-doc text output for ansible\-core before 2\.15\. If you have trouble deciphering the documentation markup\, please upgrade to ansible\-core 2\.15 \(or newer\)\, or read the HTML documentation on [https\://docs\.ansible\.com/ansible/devel/collections/community/routeros/](https\://docs\.ansible\.com/ansible/devel/collections/community/routeros/)\. + + +## v2\.8\.2 + + +### Release Summary + +Bugfix release\. + + +### Bugfixes + +* api\_modify\, api\_info \- add missing parameter tls for the tool e\-mail path \([https\://github\.com/ansible\-collections/community\.routeros/issues/179](https\://github\.com/ansible\-collections/community\.routeros/issues/179)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/180](https\://github\.com/ansible\-collections/community\.routeros/pull/180)\)\. + + +## v2\.8\.1 + + +### Release Summary + +Bugfix release\. + + +### Bugfixes + +* facts \- do not crash in CLI output preprocessing in unexpected situations during line unwrapping \([https\://github\.com/ansible\-collections/community\.routeros/issues/170](https\://github\.com/ansible\-collections/community\.routeros/issues/170)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/177](https\://github\.com/ansible\-collections/community\.routeros/pull/177)\)\. + + +## v2\.8\.0 + + +### Release Summary + +Bugfix and feature release\. + + +### Minor Changes + +* api\_modify \- adapt data for API paths ip dhcp\-server network \([https\://github\.com/ansible\-collections/community\.routeros/pull/156](https\://github\.com/ansible\-collections/community\.routeros/pull/156)\)\. +* api\_modify \- add support for API path snmp community \([https\://github\.com/ansible\-collections/community\.routeros/pull/159](https\://github\.com/ansible\-collections/community\.routeros/pull/159)\)\. +* api\_modify \- add support for trap\-interfaces in API path snmp \([https\://github\.com/ansible\-collections/community\.routeros/pull/159](https\://github\.com/ansible\-collections/community\.routeros/pull/159)\)\. +* api\_modify \- add support to disable IPv6 in API paths ipv6 settings \([https\://github\.com/ansible\-collections/community\.routeros/pull/158](https\://github\.com/ansible\-collections/community\.routeros/pull/158)\)\. +* api\_modify \- support API paths ip firewall layer7\-protocol \([https\://github\.com/ansible\-collections/community\.routeros/pull/153](https\://github\.com/ansible\-collections/community\.routeros/pull/153)\)\. +* command \- workaround for extra characters in stdout in RouterOS versions between 6\.49 and 7\.1\.5 \([https\://github\.com/ansible\-collections/community\.routeros/issues/62](https\://github\.com/ansible\-collections/community\.routeros/issues/62)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/161](https\://github\.com/ansible\-collections/community\.routeros/pull/161)\)\. + + +### Bugfixes + +* api\_info\, api\_modify \- fix default and remove behavior for dhcp\-options in path ip dhcp\-client \([https\://github\.com/ansible\-collections/community\.routeros/issues/148](https\://github\.com/ansible\-collections/community\.routeros/issues/148)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/154](https\://github\.com/ansible\-collections/community\.routeros/pull/154)\)\. +* api\_modify \- fix handling of disabled keys on creation \([https\://github\.com/ansible\-collections/community\.routeros/pull/154](https\://github\.com/ansible\-collections/community\.routeros/pull/154)\)\. +* various plugins and modules \- remove unnecessary imports \([https\://github\.com/ansible\-collections/community\.routeros/pull/149](https\://github\.com/ansible\-collections/community\.routeros/pull/149)\)\. + + +## v2\.7\.0 + + +### Release Summary + +Bugfix and feature release\. + + +### Minor Changes + +* api\_modify\, api\_info \- support API paths ip arp\, ip firewall raw\, ipv6 firewall raw \([https\://github\.com/ansible\-collections/community\.routeros/pull/144](https\://github\.com/ansible\-collections/community\.routeros/pull/144)\)\. + + +### Bugfixes + +* api\_modify\, api\_info \- defaults corrected for fields in interface wireguard peers API path \([https\://github\.com/ansible\-collections/community\.routeros/pull/144](https\://github\.com/ansible\-collections/community\.routeros/pull/144)\)\. + + +## v2\.6\.0 + + +### Release Summary + +Regular bugfix and feature release\. + + +### Minor Changes + +* api\_modify\, api\_info \- add field regexp to ip dns static \([https\://github\.com/ansible\-collections/community\.routeros/issues/141](https\://github\.com/ansible\-collections/community\.routeros/issues/141)\)\. +* api\_modify\, api\_info \- support API paths interface wireguard\, interface wireguard peers \([https\://github\.com/ansible\-collections/community\.routeros/pull/143](https\://github\.com/ansible\-collections/community\.routeros/pull/143)\)\. + + +### Bugfixes + +* api\_modify \- do not use name as a unique key in ip dns static \([https\://github\.com/ansible\-collections/community\.routeros/issues/141](https\://github\.com/ansible\-collections/community\.routeros/issues/141)\)\. +* api\_modify\, api\_info \- do not crash if router contains regexp DNS entries in ip dns static \([https\://github\.com/ansible\-collections/community\.routeros/issues/141](https\://github\.com/ansible\-collections/community\.routeros/issues/141)\)\. + + +## v2\.5\.0 + + +### Release Summary + +Feature and bugfix release\. + + +### Minor Changes + +* api\_info\, api\_modify \- support API paths interface ethernet poe\, interface gre6\, interface vrrp and also support all previously missing fields of entries in ip dhcp\-server \([https\://github\.com/ansible\-collections/community\.routeros/pull/137](https\://github\.com/ansible\-collections/community\.routeros/pull/137)\)\. + + +### Bugfixes + +* api\_modify \- address\-pool field of entries in API path ip dhcp\-server is not required anymore \([https\://github\.com/ansible\-collections/community\.routeros/pull/137](https\://github\.com/ansible\-collections/community\.routeros/pull/137)\)\. + + +## v2\.4\.0 + + +### Release Summary + +Feature release improving the api\* modules\. + + +### Minor Changes + +* api\* modules \- Add new option force\_no\_cert to connect with ADH ciphers \([https\://github\.com/ansible\-collections/community\.routeros/pull/124](https\://github\.com/ansible\-collections/community\.routeros/pull/124)\)\. +* api\_info \- new parameter include\_builtin which allows to include \"builtin\" entries that are automatically generated by ROS and cannot be modified by the user \([https\://github\.com/ansible\-collections/community\.routeros/pull/130](https\://github\.com/ansible\-collections/community\.routeros/pull/130)\)\. +* api\_modify\, api\_info \- support API paths \- interface bonding\, interface bridge mlag\, ipv6 firewall mangle\, ipv6 nd\, system scheduler\, system script\, system ups \([https\://github\.com/ansible\-collections/community\.routeros/pull/133](https\://github\.com/ansible\-collections/community\.routeros/pull/133)\)\. +* api\_modify\, api\_info \- support API paths caps\-man access\-list\, caps\-man configuration\, caps\-man datapath\, caps\-man manager\, caps\-man provisioning\, caps\-man security \([https\://github\.com/ansible\-collections/community\.routeros/pull/126](https\://github\.com/ansible\-collections/community\.routeros/pull/126)\)\. +* api\_modify\, api\_info \- support API paths interface list and interface list member \([https\://github\.com/ansible\-collections/community\.routeros/pull/120](https\://github\.com/ansible\-collections/community\.routeros/pull/120)\)\. +* api\_modify\, api\_info \- support API paths interface pppoe\-client\, interface vlan\, interface bridge\, interface bridge vlan \([https\://github\.com/ansible\-collections/community\.routeros/pull/125](https\://github\.com/ansible\-collections/community\.routeros/pull/125)\)\. +* api\_modify\, api\_info \- support API paths ip ipsec identity\, ip ipsec peer\, ip ipsec policy\, ip ipsec profile\, ip ipsec proposal \([https\://github\.com/ansible\-collections/community\.routeros/pull/129](https\://github\.com/ansible\-collections/community\.routeros/pull/129)\)\. +* api\_modify\, api\_info \- support API paths ip route and ip route vrf \([https\://github\.com/ansible\-collections/community\.routeros/pull/123](https\://github\.com/ansible\-collections/community\.routeros/pull/123)\)\. +* api\_modify\, api\_info \- support API paths ipv6 address\, ipv6 dhcp\-server\, ipv6 dhcp\-server option\, ipv6 route\, queue tree\, routing ospf area\, routing ospf area range\, routing ospf instance\, routing ospf interface\-template\, routing pimsm instance\, routing pimsm interface\-template \([https\://github\.com/ansible\-collections/community\.routeros/pull/131](https\://github\.com/ansible\-collections/community\.routeros/pull/131)\)\. +* api\_modify\, api\_info \- support API paths system logging\, system logging action \([https\://github\.com/ansible\-collections/community\.routeros/pull/127](https\://github\.com/ansible\-collections/community\.routeros/pull/127)\)\. +* api\_modify\, api\_info \- support field hw\-offload for path ip firewall filter \([https\://github\.com/ansible\-collections/community\.routeros/pull/121](https\://github\.com/ansible\-collections/community\.routeros/pull/121)\)\. +* api\_modify\, api\_info \- support fields address\-list\, address\-list\-timeout\, connection\-bytes\, connection\-limit\, connection\-mark\, connection\-rate\, connection\-type\, content\, disabled\, dscp\, dst\-address\-list\, dst\-address\-type\, dst\-limit\, fragment\, hotspot\, icmp\-options\, in\-bridge\-port\, in\-bridge\-port\-list\, ingress\-priority\, ipsec\-policy\, ipv4\-options\, jump\-target\, layer7\-protocol\, limit\, log\, log\-prefix\, nth\, out\-bridge\-port\, out\-bridge\-port\-list\, packet\-mark\, packet\-size\, per\-connection\-classifier\, port\, priority\, psd\, random\, realm\, routing\-mark\, same\-not\-by\-dst\, src\-address\, src\-address\-list\, src\-address\-type\, src\-mac\-address\, src\-port\, tcp\-mss\, time\, tls\-host\, ttl in ip firewall nat path \([https\://github\.com/ansible\-collections/community\.routeros/pull/133](https\://github\.com/ansible\-collections/community\.routeros/pull/133)\)\. +* api\_modify\, api\_info \- support fields combo\-mode\, comment\, fec\-mode\, mdix\-enable\, poe\-out\, poe\-priority\, poe\-voltage\, power\-cycle\-interval\, power\-cycle\-ping\-address\, power\-cycle\-ping\-enabled\, power\-cycle\-ping\-timeout for path interface ethernet \([https\://github\.com/ansible\-collections/community\.routeros/pull/121](https\://github\.com/ansible\-collections/community\.routeros/pull/121)\)\. +* api\_modify\, api\_info \- support fields jump\-target\, reject\-with in ip firewall filter API path\, field comment in ip firwall address\-list API path\, field jump\-target in ip firewall mangle API path\, field comment in ipv6 firewall address\-list API path\, fields jump\-target\, reject\-with in ipv6 firewall filter API path \([https\://github\.com/ansible\-collections/community\.routeros/pull/133](https\://github\.com/ansible\-collections/community\.routeros/pull/133)\)\. +* api\_modify\, api\_info \- support for API fields that can be disabled and have default value at the same time\, support API paths interface gre\, interface eoip \([https\://github\.com/ansible\-collections/community\.routeros/pull/128](https\://github\.com/ansible\-collections/community\.routeros/pull/128)\)\. +* api\_modify\, api\_info \- support for fields blackhole\, pref\-src\, routing\-table\, suppress\-hw\-offload\, type\, vrf\-interface in ip route path \([https\://github\.com/ansible\-collections/community\.routeros/pull/131](https\://github\.com/ansible\-collections/community\.routeros/pull/131)\)\. +* api\_modify\, api\_info \- support paths system ntp client servers and system ntp server available in ROS7\, as well as new fields servers\, mode\, and vrf for system ntp client \([https\://github\.com/ansible\-collections/community\.routeros/pull/122](https\://github\.com/ansible\-collections/community\.routeros/pull/122)\)\. + + +### Bugfixes + +* api\_modify \- ip route entry can be defined without the need of gateway field\, which is correct for unreachable/blackhole type of routes \([https\://github\.com/ansible\-collections/community\.routeros/pull/131](https\://github\.com/ansible\-collections/community\.routeros/pull/131)\)\. +* api\_modify \- queue interface path works now \([https\://github\.com/ansible\-collections/community\.routeros/pull/131](https\://github\.com/ansible\-collections/community\.routeros/pull/131)\)\. +* api\_modify\, api\_info \- removed wrong field dynamic from API path ipv6 firewall address\-list \([https\://github\.com/ansible\-collections/community\.routeros/pull/133](https\://github\.com/ansible\-collections/community\.routeros/pull/133)\)\. +* api\_modify\, api\_info \- the default of the field ingress\-filtering in interface bridge port is now true\, which is the default in ROS \([https\://github\.com/ansible\-collections/community\.routeros/pull/125](https\://github\.com/ansible\-collections/community\.routeros/pull/125)\)\. +* command\, facts \- commands do not timeout in safe mode anymore \([https\://github\.com/ansible\-collections/community\.routeros/pull/134](https\://github\.com/ansible\-collections/community\.routeros/pull/134)\)\. + + +### Known Issues + +* api\_modify \- when limits for entries in queue tree are defined as human readable \- for example 25M \-\, the configuration will be correctly set in ROS\, but the module will indicate the item is changed on every run even when there was no change done\. This is caused by the ROS API which returns the number in bytes \- for example 25000000 \(which is inconsistent with the CLI behavior\)\. In order to mitigate that\, the limits have to be defined in bytes \(those will still appear as human readable in the ROS CLI\) \([https\://github\.com/ansible\-collections/community\.routeros/pull/131](https\://github\.com/ansible\-collections/community\.routeros/pull/131)\)\. +* api\_modify\, api\_info \- routing ospf area\, routing ospf area range\, routing ospf instance\, routing ospf interface\-template paths are not fully implemented for ROS6 due to the significant changes between ROS6 and ROS7 \([https\://github\.com/ansible\-collections/community\.routeros/pull/131](https\://github\.com/ansible\-collections/community\.routeros/pull/131)\)\. + + +## v2\.3\.1 + + +### Release Summary + +Maintenance release with improved documentation\. + + +### Known Issues + +* The community\.routeros\.command module claims to support check mode\. Since it cannot judge whether the commands executed modify state or not\, this behavior is incorrect\. Since this potentially breaks existing playbooks\, we will not change this behavior until community\.routeros 3\.0\.0\. + + +## v2\.3\.0 + + +### Release Summary + +Feature and bugfix release\. + + +### Minor Changes + +* The collection repository conforms to the [REUSE specification](https\://reuse\.software/spec/) except for the changelog fragments \([https\://github\.com/ansible\-collections/community\.routeros/pull/108](https\://github\.com/ansible\-collections/community\.routeros/pull/108)\)\. +* api\* modules \- added timeout parameter \([https\://github\.com/ansible\-collections/community\.routeros/pull/109](https\://github\.com/ansible\-collections/community\.routeros/pull/109)\)\. +* api\_modify\, api\_info \- support API path ip firewall mangle \([https\://github\.com/ansible\-collections/community\.routeros/pull/110](https\://github\.com/ansible\-collections/community\.routeros/pull/110)\)\. + + +### Bugfixes + +* api\_modify\, api\_info \- make API path ip dhcp\-server support script\, and ip firewall nat support in\-interface and in\-interface\-list \([https\://github\.com/ansible\-collections/community\.routeros/pull/110](https\://github\.com/ansible\-collections/community\.routeros/pull/110)\)\. + + +## v2\.2\.1 + + +### Release Summary + +Bugfix release\. + + +### Bugfixes + +* api\_modify\, api\_info \- make API path ip dhcp\-server lease support server\=all \([https\://github\.com/ansible\-collections/community\.routeros/issues/104](https\://github\.com/ansible\-collections/community\.routeros/issues/104)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/107](https\://github\.com/ansible\-collections/community\.routeros/pull/107)\)\. +* api\_modify\, api\_info \- make API path ip dhcp\-server network support missing options boot\-file\-name\, dhcp\-option\-set\, dns\-none\, domain\, and next\-server \([https\://github\.com/ansible\-collections/community\.routeros/issues/104](https\://github\.com/ansible\-collections/community\.routeros/issues/104)\, [https\://github\.com/ansible\-collections/community\.routeros/pull/106](https\://github\.com/ansible\-collections/community\.routeros/pull/106)\)\. + + +## v2\.2\.0 + + +### Release Summary + +New feature release\. + + +### Minor Changes + +* All software licenses are now in the LICENSES/ directory of the collection root\. Moreover\, SPDX\-License\-Identifier\: is used to declare the applicable license for every file that is not automatically generated \([https\://github\.com/ansible\-collections/community\.routeros/pull/101](https\://github\.com/ansible\-collections/community\.routeros/pull/101)\)\. + + +### Bugfixes + +* Include LICENSES/BSD\-2\-Clause\.txt file for the routeros module utils \([https\://github\.com/ansible\-collections/community\.routeros/pull/101](https\://github\.com/ansible\-collections/community\.routeros/pull/101)\)\. + + +### New Modules + +* community\.routeros\.api\_info \- Retrieve information from API +* community\.routeros\.api\_modify \- Modify data at paths with API + + +## v2\.1\.0 + + +### Release Summary + +Feature and bugfix release with new modules\. + + +### Minor Changes + +* Added a community\.routeros\.api module defaults group\. Use with group/community\.routeros\.api to provide options for all API\-based modules \([https\://github\.com/ansible\-collections/community\.routeros/pull/89](https\://github\.com/ansible\-collections/community\.routeros/pull/89)\)\. +* Prepare collection for inclusion in an Execution Environment by declaring its dependencies \([https\://github\.com/ansible\-collections/community\.routeros/pull/83](https\://github\.com/ansible\-collections/community\.routeros/pull/83)\)\. +* api \- add new option extended query more complex queries against RouterOS API \([https\://github\.com/ansible\-collections/community\.routeros/pull/63](https\://github\.com/ansible\-collections/community\.routeros/pull/63)\)\. +* api \- update query to accept symbolic parameters \([https\://github\.com/ansible\-collections/community\.routeros/pull/63](https\://github\.com/ansible\-collections/community\.routeros/pull/63)\)\. +* api\* modules \- allow to set an encoding other than the default ASCII for communicating with the API \([https\://github\.com/ansible\-collections/community\.routeros/pull/95](https\://github\.com/ansible\-collections/community\.routeros/pull/95)\)\. + + +### Bugfixes + +* query \- fix query function check for \.id vs\. id arguments to not conflict with routeros arguments like identity \([https\://github\.com/ansible\-collections/community\.routeros/pull/68](https\://github\.com/ansible\-collections/community\.routeros/pull/68)\, [https\://github\.com/ansible\-collections/community\.routeros/issues/67](https\://github\.com/ansible\-collections/community\.routeros/issues/67)\)\. +* quoting and unquoting filter plugins\, api module \- handle the escape sequence \\\_ correctly as escaping a space and not an underscore \([https\://github\.com/ansible\-collections/community\.routeros/pull/89](https\://github\.com/ansible\-collections/community\.routeros/pull/89)\)\. + + +### New Modules + +* community\.routeros\.api\_facts \- Collect facts from remote devices running MikroTik RouterOS using the API +* community\.routeros\.api\_find\_and\_modify \- Find and modify information using the API + + +## v2\.0\.0 + + +### Release Summary + +A new major release with breaking changes in the behavior of community\.routeros\.api and community\.routeros\.command\. + + +### Minor Changes + +* api \- make validation of WHERE for query more strict \([https\://github\.com/ansible\-collections/community\.routeros/pull/53](https\://github\.com/ansible\-collections/community\.routeros/pull/53)\)\. +* command \- the commands and wait\_for options now convert the list elements to strings \([https\://github\.com/ansible\-collections/community\.routeros/pull/55](https\://github\.com/ansible\-collections/community\.routeros/pull/55)\)\. +* facts \- the gather\_subset option now converts the list elements to strings \([https\://github\.com/ansible\-collections/community\.routeros/pull/55](https\://github\.com/ansible\-collections/community\.routeros/pull/55)\)\. + + +### Breaking Changes / Porting Guide + +* api \- due to a programming error\, the module never failed on errors\. This has now been fixed\. If you are relying on the module not failing in case of idempotent commands \(resulting in errors like failure\: already have such address\)\, you need to adjust your roles/playbooks\. We suggest to use failed\_when to accept failure in specific circumstances\, for example failed\_when\: \"\'failure\: already have \' in result\.msg\[0\]\" \([https\://github\.com/ansible\-collections/community\.routeros/pull/39](https\://github\.com/ansible\-collections/community\.routeros/pull/39)\)\. +* api \- splitting commands no longer uses a naive split by whitespace\, but a more RouterOS CLI compatible splitting algorithm \([https\://github\.com/ansible\-collections/community\.routeros/pull/45](https\://github\.com/ansible\-collections/community\.routeros/pull/45)\)\. +* command \- the module now always indicates that a change happens\. If this is not correct\, please use changed\_when to determine the correct changed status for a task \([https\://github\.com/ansible\-collections/community\.routeros/pull/50](https\://github\.com/ansible\-collections/community\.routeros/pull/50)\)\. + + +### Bugfixes + +* api \- improve splitting of WHERE queries \([https\://github\.com/ansible\-collections/community\.routeros/pull/47](https\://github\.com/ansible\-collections/community\.routeros/pull/47)\)\. +* api \- when converting result lists to dictionaries\, no longer removes second \= and text following that if present \([https\://github\.com/ansible\-collections/community\.routeros/pull/47](https\://github\.com/ansible\-collections/community\.routeros/pull/47)\)\. +* routeros cliconf plugin \- adjust function signature that was modified in Ansible after creation of this plugin \([https\://github\.com/ansible\-collections/community\.routeros/pull/43](https\://github\.com/ansible\-collections/community\.routeros/pull/43)\)\. + + +### New Plugins + + +#### Filter + +* community\.routeros\.join \- Join a list of arguments to a command +* community\.routeros\.list\_to\_dict \- Convert a list of arguments to a list of dictionary +* community\.routeros\.quote\_argument \- Quote an argument +* community\.routeros\.quote\_argument\_value \- Quote an argument value +* community\.routeros\.split \- Split a command into arguments + + +## v1\.2\.0 + + +### Release Summary + +Bugfix and feature release\. + + +### Minor Changes + +* Avoid internal ansible\-core module\_utils in favor of equivalent public API available since at least Ansible 2\.9 \([https\://github\.com/ansible\-collections/community\.routeros/pull/38](https\://github\.com/ansible\-collections/community\.routeros/pull/38)\)\. +* api \- add options validate\_certs \(default value true\)\, validate\_cert\_hostname \(default value false\)\, and ca\_path to control certificate validation \([https\://github\.com/ansible\-collections/community\.routeros/pull/37](https\://github\.com/ansible\-collections/community\.routeros/pull/37)\)\. +* api \- rename option ssl to tls\, and keep the old name as an alias \([https\://github\.com/ansible\-collections/community\.routeros/pull/37](https\://github\.com/ansible\-collections/community\.routeros/pull/37)\)\. +* fact \- add fact ansible\_net\_config\_nonverbose to get idempotent config \(no date\, no verbose\) \([https\://github\.com/ansible\-collections/community\.routeros/pull/23](https\://github\.com/ansible\-collections/community\.routeros/pull/23)\)\. + + +### Bugfixes + +* api \- when using TLS/SSL\, remove explicit cipher configuration to insecure values\, which also makes it impossible to connect to newer RouterOS versions \([https\://github\.com/ansible\-collections/community\.routeros/pull/34](https\://github\.com/ansible\-collections/community\.routeros/pull/34)\)\. + + +## v1\.1\.0 + + +### Release Summary + +This release allow dashes in usernames for SSH\-based modules\. + + +### Minor Changes + +* command \- added support for a dash \(\-\) in username \([https\://github\.com/ansible\-collections/community\.routeros/pull/18](https\://github\.com/ansible\-collections/community\.routeros/pull/18)\)\. +* facts \- added support for a dash \(\-\) in username \([https\://github\.com/ansible\-collections/community\.routeros/pull/18](https\://github\.com/ansible\-collections/community\.routeros/pull/18)\)\. + + +## v1\.0\.1 + + +### Release Summary + +Maintenance release with a bugfix for api\. + + +### Bugfixes + +* api \- remove id to \.id as default requirement which conflicts with RouterOS id configuration parameter \([https\://github\.com/ansible\-collections/community\.routeros/pull/15](https\://github\.com/ansible\-collections/community\.routeros/pull/15)\)\. + + +## v1\.0\.0 + + +### Release Summary + +This is the first production \(non\-prerelease\) release of community\.routeros\. + + +### Bugfixes + +* routeros terminal plugin \- allow slashes in hostnames for terminal detection\. Without this\, slashes in hostnames will result in connection timeouts \([https\://github\.com/ansible\-collections/community\.network/pull/138](https\://github\.com/ansible\-collections/community\.network/pull/138)\)\. + + +## v0\.1\.1 + + +### Release Summary + +Small improvements and bugfixes over the initial release\. + + +### Bugfixes + +* api \- fix crash when the ssl parameter is used \([https\://github\.com/ansible\-collections/community\.routeros/pull/3](https\://github\.com/ansible\-collections/community\.routeros/pull/3)\)\. + + +## v0\.1\.0 + + +### Release Summary + +The community\.routeros continues the work on the Ansible RouterOS modules from their state in community\.network 1\.2\.0\. The changes listed here are thus relative to the modules community\.network\.routeros\_\*\. + + +### Minor Changes + +* facts \- now also collecting data about BGP and OSPF \([https\://github\.com/ansible\-collections/community\.network/pull/101](https\://github\.com/ansible\-collections/community\.network/pull/101)\)\. +* facts \- set configuration export on to verbose\, for full configuration export \([https\://github\.com/ansible\-collections/community\.network/pull/104](https\://github\.com/ansible\-collections/community\.network/pull/104)\)\. diff --git a/CHANGELOG.md.license b/CHANGELOG.md.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/CHANGELOG.md.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2b6ed37..3fc3b05 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,19 +4,737 @@ Community RouterOS Release Notes .. contents:: Topics - -v2.0.0-a1 -========= +v3.9.0 +====== Release Summary --------------- -First prerelease for a new major release with a breaking change in the behavior of ``community.routeros.api``. +Bugfix and feature release. + +Minor Changes +------------- + +- api_info, api modify - add ``remote-log-format``, ``remote-protocol``, and ``event-delimiter`` to ``system logging action`` (https://github.com/ansible-collections/community.routeros/pull/381). +- api_info, api_modify - add ``disable-link-local-address`` and ``stale-neighbor-timeout`` fields to ``ipv6 settings`` (https://github.com/ansible-collections/community.routeros/pull/380). +- api_info, api_modify - adjust neighbor limit fields in ``ipv6 settings`` to match RouterOS 7.18 and newer (https://github.com/ansible-collections/community.routeros/pull/380). +- api_info, api_modify - set ``passthrough`` default in ``ip firewall mangle`` to ``true`` for RouterOS 7.19 and newer (https://github.com/ansible-collections/community.routeros/pull/382). +- api_info, api_modify - since RouterOS 7.17 VRF is supported for OVPN server. It now supports multiple entries, while ``api_modify`` so far only accepted a single entry. The ``interface ovpn-server server`` path now allows multiple entries on RouterOS 7.17 and newer (https://github.com/ansible-collections/community.routeros/pull/383). + +Bugfixes +-------- + +- routeros terminal plugin - fix ``terminal_stdout_re`` pattern to handle long system identities when connecting to RouterOS through SSH (https://github.com/ansible-collections/community.routeros/pull/386). + +v3.8.1 +====== + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- facts and api_facts modules - prevent deprecation warnings when used with ansible-core 2.19 (https://github.com/ansible-collections/community.routeros/pull/384). + +v3.8.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add ``interface ethernet switch port-isolation`` which is supported since RouterOS 6.43 (https://github.com/ansible-collections/community.routeros/pull/375). +- api_info, api_modify - add ``routing bfd configuration``. Officially stabilized BFD support for BGP and OSPF is available since RouterOS 7.11 + (https://github.com/ansible-collections/community.routeros/pull/375). +- api_modify, api_info - support API path ``ip ipsec mode-config`` (https://github.com/ansible-collections/community.routeros/pull/376). + +v3.7.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_find_and_modify - allow to control whether ``dynamic`` and/or ``builtin`` entries are ignored with the new ``ignore_dynamic`` and ``ignore_builtin`` options (https://github.com/ansible-collections/community.routeros/issues/372, https://github.com/ansible-collections/community.routeros/pull/373). +- api_info, api_modify - add ``port-cost-mode`` to ``interface bridge`` which is supported since RouterOS 7.13 (https://github.com/ansible-collections/community.routeros/pull/371). + +v3.6.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add ``mdns-repeat-ifaces`` to ``ip dns`` for RouterOS 7.16 and newer (https://github.com/ansible-collections/community.routeros/pull/358). +- api_info, api_modify - field name change in ``routing bgp connection`` path implemented by RouterOS 7.19 and newer (https://github.com/ansible-collections/community.routeros/pull/360). +- api_info, api_modify - rename ``is-responder`` property in ``interface wireguard peers`` to ``responder`` for RouterOS 7.17 and newer (https://github.com/ansible-collections/community.routeros/pull/364). + +v3.5.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - change default for ``/ip/cloud/ddns-enabled`` for RouterOS 7.17 and newer from ``yes`` to ``auto`` (https://github.com/ansible-collections/community.routeros/pull/350). + +v3.4.0 +====== + +Release Summary +--------------- + +Feature and bugfix release. + +Minor Changes +------------- + +- api_info, api_modify - add support for the ``ip dns forwarders`` path implemented by RouterOS 7.17 and newer (https://github.com/ansible-collections/community.routeros/pull/343). + +Bugfixes +-------- + +- api_info, api_modify - remove the primary key ``action`` from the ``interface wifi provisioning`` path, since RouterOS also allows to create completely duplicate entries (https://github.com/ansible-collections/community.routeros/issues/344, https://github.com/ansible-collections/community.routeros/pull/345). + +v3.3.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add missing attribute ``require-message-auth`` for the ``radius`` path which exists since RouterOS version 7.15 (https://github.com/ansible-collections/community.routeros/issues/338, https://github.com/ansible-collections/community.routeros/pull/339). +- api_info, api_modify - add the ``interface 6to4`` path. Used to manage IPv6 tunnels via tunnel-brokers like HE, where native IPv6 is not provided (https://github.com/ansible-collections/community.routeros/pull/342). +- api_info, api_modify - add the ``interface wireless access-list`` and ``interface wireless connect-list`` paths (https://github.com/ansible-collections/community.routeros/issues/284, https://github.com/ansible-collections/community.routeros/pull/340). +- api_info, api_modify - add the ``use-interface-duid`` option for ``ipv6 dhcp-client`` path. This option prevents issues with Fritzbox modems and routers, when using virtual interfaces (like VLANs) may create duplicated records in hosts config, this breaks original "expose-host" function. Also add the ``script``, ``custom-duid`` and ``validate-server-duid`` as backport from 7.15 version update (https://github.com/ansible-collections/community.routeros/pull/341). + +v3.2.0 +====== + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add support for the ``routing filter community-list`` path implemented by RouterOS 7 and newer (https://github.com/ansible-collections/community.routeros/pull/331). + +v3.1.0 +====== + +Release Summary +--------------- + +Bugfix and feature release. + +Minor Changes +------------- + +- api_info, api_modify - add missing fields ``comment``, ``next-pool`` to ``ip pool`` path (https://github.com/ansible-collections/community.routeros/pull/327). + +Bugfixes +-------- + +- api_info, api_modify - fields ``log`` and ``log-prefix`` in paths ``ip firewall filter``, ``ip firewall mangle``, ``ip firewall nat``, ``ip firewall raw`` now have the correct default values (https://github.com/ansible-collections/community.routeros/pull/324). + +v3.0.0 +====== + +Release Summary +--------------- + +Major release that drops support for End of Life Python versions and fixes check mode for community.routeros.command. + +Breaking Changes / Porting Guide +-------------------------------- + +- command - the module no longer declares that it supports check mode (https://github.com/ansible-collections/community.routeros/pull/318). + +Removed Features (previously deprecated) +---------------------------------------- + +- The collection no longer supports Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13, and ansible-core 2.14. If you need to continue using End of Life versions of Ansible/ansible-base/ansible-core, please use community.routeros 2.x.y (https://github.com/ansible-collections/community.routeros/pull/318). + +v2.20.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add new parameters from the RouterOS 7.16 release (https://github.com/ansible-collections/community.routeros/pull/323). +- api_info, api_modify - add support ``interface l2tp-client`` configuration (https://github.com/ansible-collections/community.routeros/pull/322). +- api_info, api_modify - add support for the ``cpu-frequency``, ``memory-frequency``, ``preboot-etherboot`` and ``preboot-etherboot-server`` properties in ``system routerboard settings`` (https://github.com/ansible-collections/community.routeros/pull/320). +- api_info, api_modify - add support for the ``matching-type`` property in ``ip dhcp-server matcher`` introduced by RouterOS 7.16 (https://github.com/ansible-collections/community.routeros/pull/321). + +v2.19.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add support for the ``ip dns adlist`` path implemented by RouterOS 7.15 and newer (https://github.com/ansible-collections/community.routeros/pull/310). +- api_info, api_modify - add support for the ``mld-version`` and ``multicast-querier`` properties in ``interface bridge`` (https://github.com/ansible-collections/community.routeros/pull/315). +- api_info, api_modify - add support for the ``routing filter num-list`` path implemented by RouterOS 7 and newer (https://github.com/ansible-collections/community.routeros/pull/313). +- api_info, api_modify - add support for the ``routing igmp-proxy`` path (https://github.com/ansible-collections/community.routeros/pull/309). +- api_modify, api_info - add read-only ``default`` field to ``snmp community`` (https://github.com/ansible-collections/community.routeros/pull/311). + +v2.18.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info - allow to restrict the output by limiting fields to specific values with the new ``restrict`` option (https://github.com/ansible-collections/community.routeros/pull/305). +- api_info, api_modify - add support for the ``ip dhcp-server matcher`` path (https://github.com/ansible-collections/community.routeros/pull/300). +- api_info, api_modify - add support for the ``ipv6 nd prefix`` path (https://github.com/ansible-collections/community.routeros/pull/303). +- api_info, api_modify - add support for the ``name`` and ``is-responder`` properties under the ``interface wireguard peers`` path introduced in RouterOS 7.15 (https://github.com/ansible-collections/community.routeros/pull/304). +- api_info, api_modify - add support for the ``routing ospf static-neighbor`` path in RouterOS 7 (https://github.com/ansible-collections/community.routeros/pull/302). +- api_info, api_modify - set default for ``force`` in ``ip dhcp-server option`` to an explicit ``false`` (https://github.com/ansible-collections/community.routeros/pull/300). +- api_modify - allow to restrict what is updated by limiting fields to specific values with the new ``restrict`` option (https://github.com/ansible-collections/community.routeros/pull/305). + +Deprecated Features +------------------- + +- The collection deprecates support for all Ansible/ansible-base/ansible-core versions that are currently End of Life, `according to the ansible-core support matrix `__. This means that the next major release of the collection will no longer support Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13, and ansible-core 2.14. + +Bugfixes +-------- + +- api_modify, api_info - change the default of ``ingress-filtering`` in paths ``interface bridge`` and ``interface bridge port`` back to ``false`` for RouterOS before version 7 (https://github.com/ansible-collections/community.routeros/pull/305). + +v2.17.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add ``system health settings`` path (https://github.com/ansible-collections/community.routeros/pull/294). +- api_info, api_modify - add missing path ``/system resource irq rps`` (https://github.com/ansible-collections/community.routeros/pull/295). +- api_info, api_modify - add parameter ``host-key-type`` for ``ip ssh`` path (https://github.com/ansible-collections/community.routeros/issues/280, https://github.com/ansible-collections/community.routeros/pull/297). + +v2.16.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add missing path ``/ppp secret`` (https://github.com/ansible-collections/community.routeros/pull/286). +- api_info, api_modify - minor changes ``/interface ethernet`` path fields (https://github.com/ansible-collections/community.routeros/pull/288). + +v2.15.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - Add RouterOS 7.x support to ``/mpls ldp`` path (https://github.com/ansible-collections/community.routeros/pull/271). +- api_info, api_modify - add ``/ip route rule`` path for RouterOS 6.x (https://github.com/ansible-collections/community.routeros/pull/278). +- api_info, api_modify - add ``/routing filter`` path for RouterOS 6.x (https://github.com/ansible-collections/community.routeros/pull/279). +- api_info, api_modify - add default value for ``from-pool`` field in ``/ipv6 address`` (https://github.com/ansible-collections/community.routeros/pull/270). +- api_info, api_modify - add missing path ``/interface pppoe-server server`` (https://github.com/ansible-collections/community.routeros/pull/273). +- api_info, api_modify - add missing path ``/ip dhcp-relay`` (https://github.com/ansible-collections/community.routeros/pull/276). +- api_info, api_modify - add missing path ``/queue simple`` (https://github.com/ansible-collections/community.routeros/pull/269). +- api_info, api_modify - add missing path ``/queue type`` (https://github.com/ansible-collections/community.routeros/pull/274). +- api_info, api_modify - add missing paths ``/routing bgp aggregate``, ``/routing bgp network`` and ``/routing bgp peer`` (https://github.com/ansible-collections/community.routeros/pull/277). +- api_info, api_modify - add support for paths ``/mpls interface``, ``/mpls ldp accept-filter``, ``/mpls ldp advertise-filter`` and ``mpls ldp interface`` (https://github.com/ansible-collections/community.routeros/pull/272). + +v2.14.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add read-only fields ``installed-version``, ``latest-version`` and ``status`` in ``system package update`` (https://github.com/ansible-collections/community.routeros/pull/263). +- api_info, api_modify - added support for ``interface wifi`` and its sub-paths (https://github.com/ansible-collections/community.routeros/pull/266). +- api_info, api_modify - remove default value for read-only ``running`` field in ``interface wireless`` (https://github.com/ansible-collections/community.routeros/pull/264). + +v2.13.0 +======= + +Release Summary +--------------- + +Bugfix and feature release. + +Minor Changes +------------- + +- api_info, api_modify - make path ``user group`` modifiable and add ``comment`` attribute (https://github.com/ansible-collections/community.routeros/issues/256, https://github.com/ansible-collections/community.routeros/pull/257). +- api_modify, api_info - add support for the ``ip vrf`` path in RouterOS 7 (https://github.com/ansible-collections/community.routeros/pull/259) + +Bugfixes +-------- + +- facts - fix date not getting removed for idempotent config export (https://github.com/ansible-collections/community.routeros/pull/262). + +v2.12.0 +======= + +Release Summary +--------------- + +Feature release. + +Minor Changes +------------- + +- api_info, api_modify - add ``interface ovpn-client`` path (https://github.com/ansible-collections/community.routeros/issues/242, https://github.com/ansible-collections/community.routeros/pull/244). +- api_info, api_modify - add ``radius`` path (https://github.com/ansible-collections/community.routeros/issues/241, https://github.com/ansible-collections/community.routeros/pull/245). +- api_info, api_modify - add ``routing rule`` path (https://github.com/ansible-collections/community.routeros/issues/162, https://github.com/ansible-collections/community.routeros/pull/246). +- api_info, api_modify - add missing path ``routing bgp template`` (https://github.com/ansible-collections/community.routeros/pull/243). +- api_info, api_modify - add support for the ``tx-power`` attribute in ``interface wireless`` (https://github.com/ansible-collections/community.routeros/pull/239). +- api_info, api_modify - removed ``host`` primary key in ``tool netwatch`` path (https://github.com/ansible-collections/community.routeros/pull/248). +- api_modify, api_info - added support for ``interface wifiwave2`` (https://github.com/ansible-collections/community.routeros/pull/226). + +v2.11.0 +======= + +Release Summary +--------------- + +Feature and bugfix release. + +Minor Changes +------------- + +- api_info, api_modify - add missing DoH parameters ``doh-max-concurrent-queries``, ``doh-max-server-connections``, and ``doh-timeout`` to the ``ip dns`` path (https://github.com/ansible-collections/community.routeros/issues/230, https://github.com/ansible-collections/community.routeros/pull/235) +- api_info, api_modify - add missing parameters ``address-list``, ``address-list-timeout``, ``randomise-ports``, and ``realm`` to subpaths of the ``ip firewall`` path (https://github.com/ansible-collections/community.routeros/issues/236, https://github.com/ansible-collections/community.routeros/pull/237). +- api_info, api_modify - mark the ``interface wireless`` parameter ``running`` as read-only (https://github.com/ansible-collections/community.routeros/pull/233). +- api_info, api_modify - set the default value to ``false`` for the ``disabled`` parameter in some more paths where it can be seen in the documentation (https://github.com/ansible-collections/community.routeros/pull/237). +- api_modify - add missing ``comment`` attribute to ``/routing id`` (https://github.com/ansible-collections/community.routeros/pull/234). +- api_modify - add missing attributes to the ``routing bgp connection`` path (https://github.com/ansible-collections/community.routeros/pull/234). +- api_modify - add versioning to the ``/tool e-mail`` path (RouterOS 7.12 release) (https://github.com/ansible-collections/community.routeros/pull/234). +- api_modify - make ``/ip traffic-flow target`` a multiple value attribute (https://github.com/ansible-collections/community.routeros/pull/234). + +v2.10.0 +======= + +Release Summary +--------------- + +Bugfix and feature release. + +Minor Changes +------------- + +- api_info - add new ``include_read_only`` option to select behavior for read-only values. By default these are not returned (https://github.com/ansible-collections/community.routeros/pull/213). +- api_info, api_modify - add support for ``address-list`` and ``match-subdomain`` introduced by RouterOS 7.7 in the ``ip dns static`` path (https://github.com/ansible-collections/community.routeros/pull/197). +- api_info, api_modify - add support for ``user``, ``time`` and ``gmt-offset`` under the ``system clock`` path (https://github.com/ansible-collections/community.routeros/pull/210). +- api_info, api_modify - add support for the ``interface ppp-client`` path (https://github.com/ansible-collections/community.routeros/pull/199). +- api_info, api_modify - add support for the ``interface wireless`` path (https://github.com/ansible-collections/community.routeros/pull/195). +- api_info, api_modify - add support for the ``iot modbus`` path (https://github.com/ansible-collections/community.routeros/pull/205). +- api_info, api_modify - add support for the ``ip dhcp-server option`` and ``ip dhcp-server option sets`` paths (https://github.com/ansible-collections/community.routeros/pull/223). +- api_info, api_modify - add support for the ``ip upnp interfaces``, ``tool graphing interface``, ``tool graphing resource`` paths (https://github.com/ansible-collections/community.routeros/pull/227). +- api_info, api_modify - add support for the ``ipv6 firewall nat`` path (https://github.com/ansible-collections/community.routeros/pull/204). +- api_info, api_modify - add support for the ``mode`` property in ``ip neighbor discovery-settings`` introduced in RouterOS 7.7 (https://github.com/ansible-collections/community.routeros/pull/198). +- api_info, api_modify - add support for the ``port remote-access`` path (https://github.com/ansible-collections/community.routeros/pull/224). +- api_info, api_modify - add support for the ``routing filter rule`` and ``routing filter select-rule`` paths (https://github.com/ansible-collections/community.routeros/pull/200). +- api_info, api_modify - add support for the ``routing table`` path in RouterOS 7 (https://github.com/ansible-collections/community.routeros/pull/215). +- api_info, api_modify - add support for the ``tool netwatch`` path in RouterOS 7 (https://github.com/ansible-collections/community.routeros/pull/216). +- api_info, api_modify - add support for the ``user settings`` path (https://github.com/ansible-collections/community.routeros/pull/201). +- api_info, api_modify - add support for the ``user`` path (https://github.com/ansible-collections/community.routeros/pull/211). +- api_info, api_modify - finalize fields for the ``interface wireless security-profiles`` path and enable it (https://github.com/ansible-collections/community.routeros/pull/203). +- api_info, api_modify - finalize fields for the ``ppp profile`` path and enable it (https://github.com/ansible-collections/community.routeros/pull/217). +- api_modify - add new ``handle_read_only`` and ``handle_write_only`` options to handle the module's behavior for read-only and write-only fields (https://github.com/ansible-collections/community.routeros/pull/213). +- api_modify, api_info - support API paths ``routing id``, ``routing bgp connection`` (https://github.com/ansible-collections/community.routeros/pull/220). + +Bugfixes +-------- + +- api_info, api_modify - in the ``snmp`` path, ensure that ``engine-id-suffix`` is only available on RouterOS 7.10+, and that ``engine-id`` is read-only on RouterOS 7.10+ (https://github.com/ansible-collections/community.routeros/issues/208, https://github.com/ansible-collections/community.routeros/pull/218). + +v2.9.0 +====== + +Release Summary +--------------- + +Bugfix and feature release. + +Minor Changes +------------- + +- api_info, api_modify - add path ``caps-man channel`` and enable path ``caps-man manager interface`` (https://github.com/ansible-collections/community.routeros/issues/193, https://github.com/ansible-collections/community.routeros/pull/194). +- api_info, api_modify - add path ``ip traffic-flow target`` (https://github.com/ansible-collections/community.routeros/issues/191, https://github.com/ansible-collections/community.routeros/pull/192). + +Bugfixes +-------- + +- api_modify, api_info - add missing parameter ``engine-id-suffix`` for the ``snmp`` path (https://github.com/ansible-collections/community.routeros/issues/189, https://github.com/ansible-collections/community.routeros/pull/190). + +v2.8.3 +====== + +Release Summary +--------------- + +Maintenance release with updated documentation. + +From this version on, community.routeros is using the new `Ansible semantic markup +`__ +in its documentation. If you look at documentation with the ansible-doc CLI tool +from ansible-core before 2.15, please note that it does not render the markup +correctly. You should be still able to read it in most cases, but you need +ansible-core 2.15 or later to see it as it is intended. Alternatively you can +look at `the devel docsite `__ +for the rendered HTML version of the documentation of the latest release. + +Known Issues +------------ + +- Ansible markup will show up in raw form on ansible-doc text output for ansible-core before 2.15. If you have trouble deciphering the documentation markup, please upgrade to ansible-core 2.15 (or newer), or read the HTML documentation on https://docs.ansible.com/ansible/devel/collections/community/routeros/. + +v2.8.2 +====== + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- api_modify, api_info - add missing parameter ``tls`` for the ``tool e-mail`` path (https://github.com/ansible-collections/community.routeros/issues/179, https://github.com/ansible-collections/community.routeros/pull/180). + +v2.8.1 +====== + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- facts - do not crash in CLI output preprocessing in unexpected situations during line unwrapping (https://github.com/ansible-collections/community.routeros/issues/170, https://github.com/ansible-collections/community.routeros/pull/177). + +v2.8.0 +====== + +Release Summary +--------------- + +Bugfix and feature release. + +Minor Changes +------------- + +- api_modify - adapt data for API paths ``ip dhcp-server network`` (https://github.com/ansible-collections/community.routeros/pull/156). +- api_modify - add support for API path ``snmp community`` (https://github.com/ansible-collections/community.routeros/pull/159). +- api_modify - add support for ``trap-interfaces`` in API path ``snmp`` (https://github.com/ansible-collections/community.routeros/pull/159). +- api_modify - add support to disable IPv6 in API paths ``ipv6 settings`` (https://github.com/ansible-collections/community.routeros/pull/158). +- api_modify - support API paths ``ip firewall layer7-protocol`` (https://github.com/ansible-collections/community.routeros/pull/153). +- command - workaround for extra characters in stdout in RouterOS versions between 6.49 and 7.1.5 (https://github.com/ansible-collections/community.routeros/issues/62, https://github.com/ansible-collections/community.routeros/pull/161). + +Bugfixes +-------- + +- api_info, api_modify - fix default and remove behavior for ``dhcp-options`` in path ``ip dhcp-client`` (https://github.com/ansible-collections/community.routeros/issues/148, https://github.com/ansible-collections/community.routeros/pull/154). +- api_modify - fix handling of disabled keys on creation (https://github.com/ansible-collections/community.routeros/pull/154). +- various plugins and modules - remove unnecessary imports (https://github.com/ansible-collections/community.routeros/pull/149). + +v2.7.0 +====== + +Release Summary +--------------- + +Bugfix and feature release. + +Minor Changes +------------- + +- api_modify, api_info - support API paths ``ip arp``, ``ip firewall raw``, ``ipv6 firewall raw`` (https://github.com/ansible-collections/community.routeros/pull/144). + +Bugfixes +-------- + +- api_modify, api_info - defaults corrected for fields in ``interface wireguard peers`` API path (https://github.com/ansible-collections/community.routeros/pull/144). + +v2.6.0 +====== + +Release Summary +--------------- + +Regular bugfix and feature release. + +Minor Changes +------------- + +- api_modify, api_info - add field ``regexp`` to ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). +- api_modify, api_info - support API paths ``interface wireguard``, ``interface wireguard peers`` (https://github.com/ansible-collections/community.routeros/pull/143). + +Bugfixes +-------- + +- api_modify - do not use ``name`` as a unique key in ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). +- api_modify, api_info - do not crash if router contains ``regexp`` DNS entries in ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). + +v2.5.0 +====== + +Release Summary +--------------- + +Feature and bugfix release. + +Minor Changes +------------- + +- api_info, api_modify - support API paths ``interface ethernet poe``, ``interface gre6``, ``interface vrrp`` and also support all previously missing fields of entries in ``ip dhcp-server`` (https://github.com/ansible-collections/community.routeros/pull/137). + +Bugfixes +-------- + +- api_modify - ``address-pool`` field of entries in API path ``ip dhcp-server`` is not required anymore (https://github.com/ansible-collections/community.routeros/pull/137). + +v2.4.0 +====== + +Release Summary +--------------- + +Feature release improving the ``api*`` modules. + +Minor Changes +------------- + +- api* modules - Add new option ``force_no_cert`` to connect with ADH ciphers (https://github.com/ansible-collections/community.routeros/pull/124). +- api_info - new parameter ``include_builtin`` which allows to include "builtin" entries that are automatically generated by ROS and cannot be modified by the user (https://github.com/ansible-collections/community.routeros/pull/130). +- api_modify, api_info - support API paths - ``interface bonding``, ``interface bridge mlag``, ``ipv6 firewall mangle``, ``ipv6 nd``, ``system scheduler``, ``system script``, ``system ups`` (https://github.com/ansible-collections/community.routeros/pull/133). +- api_modify, api_info - support API paths ``caps-man access-list``, ``caps-man configuration``, ``caps-man datapath``, ``caps-man manager``, ``caps-man provisioning``, ``caps-man security`` (https://github.com/ansible-collections/community.routeros/pull/126). +- api_modify, api_info - support API paths ``interface list`` and ``interface list member`` (https://github.com/ansible-collections/community.routeros/pull/120). +- api_modify, api_info - support API paths ``interface pppoe-client``, ``interface vlan``, ``interface bridge``, ``interface bridge vlan`` (https://github.com/ansible-collections/community.routeros/pull/125). +- api_modify, api_info - support API paths ``ip ipsec identity``, ``ip ipsec peer``, ``ip ipsec policy``, ``ip ipsec profile``, ``ip ipsec proposal`` (https://github.com/ansible-collections/community.routeros/pull/129). +- api_modify, api_info - support API paths ``ip route`` and ``ip route vrf`` (https://github.com/ansible-collections/community.routeros/pull/123). +- api_modify, api_info - support API paths ``ipv6 address``, ``ipv6 dhcp-server``, ``ipv6 dhcp-server option``, ``ipv6 route``, ``queue tree``, ``routing ospf area``, ``routing ospf area range``, ``routing ospf instance``, ``routing ospf interface-template``, ``routing pimsm instance``, ``routing pimsm interface-template`` (https://github.com/ansible-collections/community.routeros/pull/131). +- api_modify, api_info - support API paths ``system logging``, ``system logging action`` (https://github.com/ansible-collections/community.routeros/pull/127). +- api_modify, api_info - support field ``hw-offload`` for path ``ip firewall filter`` (https://github.com/ansible-collections/community.routeros/pull/121). +- api_modify, api_info - support fields ``address-list``, ``address-list-timeout``, ``connection-bytes``, ``connection-limit``, ``connection-mark``, ``connection-rate``, ``connection-type``, ``content``, ``disabled``, ``dscp``, ``dst-address-list``, ``dst-address-type``, ``dst-limit``, ``fragment``, ``hotspot``, ``icmp-options``, ``in-bridge-port``, ``in-bridge-port-list``, ``ingress-priority``, ``ipsec-policy``, ``ipv4-options``, ``jump-target``, ``layer7-protocol``, ``limit``, ``log``, ``log-prefix``, ``nth``, ``out-bridge-port``, ``out-bridge-port-list``, ``packet-mark``, ``packet-size``, ``per-connection-classifier``, ``port``, ``priority``, ``psd``, ``random``, ``realm``, ``routing-mark``, ``same-not-by-dst``, ``src-address``, ``src-address-list``, ``src-address-type``, ``src-mac-address``, ``src-port``, ``tcp-mss``, ``time``, ``tls-host``, ``ttl`` in ``ip firewall nat`` path (https://github.com/ansible-collections/community.routeros/pull/133). +- api_modify, api_info - support fields ``combo-mode``, ``comment``, ``fec-mode``, ``mdix-enable``, ``poe-out``, ``poe-priority``, ``poe-voltage``, ``power-cycle-interval``, ``power-cycle-ping-address``, ``power-cycle-ping-enabled``, ``power-cycle-ping-timeout`` for path ``interface ethernet`` (https://github.com/ansible-collections/community.routeros/pull/121). +- api_modify, api_info - support fields ``jump-target``, ``reject-with`` in ``ip firewall filter`` API path, field ``comment`` in ``ip firwall address-list`` API path, field ``jump-target`` in ``ip firewall mangle`` API path, field ``comment`` in ``ipv6 firewall address-list`` API path, fields ``jump-target``, ``reject-with`` in ``ipv6 firewall filter`` API path (https://github.com/ansible-collections/community.routeros/pull/133). +- api_modify, api_info - support for API fields that can be disabled and have default value at the same time, support API paths ``interface gre``, ``interface eoip`` (https://github.com/ansible-collections/community.routeros/pull/128). +- api_modify, api_info - support for fields ``blackhole``, ``pref-src``, ``routing-table``, ``suppress-hw-offload``, ``type``, ``vrf-interface`` in ``ip route`` path (https://github.com/ansible-collections/community.routeros/pull/131). +- api_modify, api_info - support paths ``system ntp client servers`` and ``system ntp server`` available in ROS7, as well as new fields ``servers``, ``mode``, and ``vrf`` for ``system ntp client`` (https://github.com/ansible-collections/community.routeros/pull/122). + +Bugfixes +-------- + +- api_modify - ``ip route`` entry can be defined without the need of ``gateway`` field, which is correct for unreachable/blackhole type of routes (https://github.com/ansible-collections/community.routeros/pull/131). +- api_modify - ``queue interface`` path works now (https://github.com/ansible-collections/community.routeros/pull/131). +- api_modify, api_info - removed wrong field ``dynamic`` from API path ``ipv6 firewall address-list`` (https://github.com/ansible-collections/community.routeros/pull/133). +- api_modify, api_info - the default of the field ``ingress-filtering`` in ``interface bridge port`` is now ``true``, which is the default in ROS (https://github.com/ansible-collections/community.routeros/pull/125). +- command, facts - commands do not timeout in safe mode anymore (https://github.com/ansible-collections/community.routeros/pull/134). + +Known Issues +------------ + +- api_modify - when limits for entries in ``queue tree`` are defined as human readable - for example ``25M`` -, the configuration will be correctly set in ROS, but the module will indicate the item is changed on every run even when there was no change done. This is caused by the ROS API which returns the number in bytes - for example ``25000000`` (which is inconsistent with the CLI behavior). In order to mitigate that, the limits have to be defined in bytes (those will still appear as human readable in the ROS CLI) (https://github.com/ansible-collections/community.routeros/pull/131). +- api_modify, api_info - ``routing ospf area``, ``routing ospf area range``, ``routing ospf instance``, ``routing ospf interface-template`` paths are not fully implemented for ROS6 due to the significant changes between ROS6 and ROS7 (https://github.com/ansible-collections/community.routeros/pull/131). + +v2.3.1 +====== + +Release Summary +--------------- + +Maintenance release with improved documentation. + +Known Issues +------------ + +- The ``community.routeros.command`` module claims to support check mode. Since it cannot judge whether the commands executed modify state or not, this behavior is incorrect. Since this potentially breaks existing playbooks, we will not change this behavior until community.routeros 3.0.0. + +v2.3.0 +====== + +Release Summary +--------------- + +Feature and bugfix release. + +Minor Changes +------------- + +- The collection repository conforms to the `REUSE specification `__ except for the changelog fragments (https://github.com/ansible-collections/community.routeros/pull/108). +- api* modules - added ``timeout`` parameter (https://github.com/ansible-collections/community.routeros/pull/109). +- api_modify, api_info - support API path ``ip firewall mangle`` (https://github.com/ansible-collections/community.routeros/pull/110). + +Bugfixes +-------- + +- api_modify, api_info - make API path ``ip dhcp-server`` support ``script``, and ``ip firewall nat`` support ``in-interface`` and ``in-interface-list`` (https://github.com/ansible-collections/community.routeros/pull/110). + +v2.2.1 +====== + +Release Summary +--------------- + +Bugfix release. + +Bugfixes +-------- + +- api_modify, api_info - make API path ``ip dhcp-server lease`` support ``server=all`` (https://github.com/ansible-collections/community.routeros/issues/104, https://github.com/ansible-collections/community.routeros/pull/107). +- api_modify, api_info - make API path ``ip dhcp-server network`` support missing options ``boot-file-name``, ``dhcp-option-set``, ``dns-none``, ``domain``, and ``next-server`` (https://github.com/ansible-collections/community.routeros/issues/104, https://github.com/ansible-collections/community.routeros/pull/106). + +v2.2.0 +====== + +Release Summary +--------------- + +New feature release. + +Minor Changes +------------- + +- All software licenses are now in the ``LICENSES/`` directory of the collection root. Moreover, ``SPDX-License-Identifier:`` is used to declare the applicable license for every file that is not automatically generated (https://github.com/ansible-collections/community.routeros/pull/101). + +Bugfixes +-------- + +- Include ``LICENSES/BSD-2-Clause.txt`` file for the ``routeros`` module utils (https://github.com/ansible-collections/community.routeros/pull/101). + +New Modules +----------- + +- community.routeros.api_info - Retrieve information from API +- community.routeros.api_modify - Modify data at paths with API + +v2.1.0 +====== + +Release Summary +--------------- + +Feature and bugfix release with new modules. + +Minor Changes +------------- + +- Added a ``community.routeros.api`` module defaults group. Use with ``group/community.routeros.api`` to provide options for all API-based modules (https://github.com/ansible-collections/community.routeros/pull/89). +- Prepare collection for inclusion in an Execution Environment by declaring its dependencies (https://github.com/ansible-collections/community.routeros/pull/83). +- api - add new option ``extended query`` more complex queries against RouterOS API (https://github.com/ansible-collections/community.routeros/pull/63). +- api - update ``query`` to accept symbolic parameters (https://github.com/ansible-collections/community.routeros/pull/63). +- api* modules - allow to set an encoding other than the default ASCII for communicating with the API (https://github.com/ansible-collections/community.routeros/pull/95). + +Bugfixes +-------- + +- query - fix query function check for ``.id`` vs. ``id`` arguments to not conflict with routeros arguments like ``identity`` (https://github.com/ansible-collections/community.routeros/pull/68, https://github.com/ansible-collections/community.routeros/issues/67). +- quoting and unquoting filter plugins, api module - handle the escape sequence ``\_`` correctly as escaping a space and not an underscore (https://github.com/ansible-collections/community.routeros/pull/89). + +New Modules +----------- + +- community.routeros.api_facts - Collect facts from remote devices running MikroTik RouterOS using the API +- community.routeros.api_find_and_modify - Find and modify information using the API + +v2.0.0 +====== + +Release Summary +--------------- + +A new major release with breaking changes in the behavior of ``community.routeros.api`` and ``community.routeros.command``. + +Minor Changes +------------- + +- api - make validation of ``WHERE`` for ``query`` more strict (https://github.com/ansible-collections/community.routeros/pull/53). +- command - the ``commands`` and ``wait_for`` options now convert the list elements to strings (https://github.com/ansible-collections/community.routeros/pull/55). +- facts - the ``gather_subset`` option now converts the list elements to strings (https://github.com/ansible-collections/community.routeros/pull/55). Breaking Changes / Porting Guide -------------------------------- - api - due to a programming error, the module never failed on errors. This has now been fixed. If you are relying on the module not failing in case of idempotent commands (resulting in errors like ``failure: already have such address``), you need to adjust your roles/playbooks. We suggest to use ``failed_when`` to accept failure in specific circumstances, for example ``failed_when: "'failure: already have ' in result.msg[0]"`` (https://github.com/ansible-collections/community.routeros/pull/39). +- api - splitting commands no longer uses a naive split by whitespace, but a more RouterOS CLI compatible splitting algorithm (https://github.com/ansible-collections/community.routeros/pull/45). +- command - the module now always indicates that a change happens. If this is not correct, please use ``changed_when`` to determine the correct changed status for a task (https://github.com/ansible-collections/community.routeros/pull/50). + +Bugfixes +-------- + +- api - improve splitting of ``WHERE`` queries (https://github.com/ansible-collections/community.routeros/pull/47). +- api - when converting result lists to dictionaries, no longer removes second ``=`` and text following that if present (https://github.com/ansible-collections/community.routeros/pull/47). +- routeros cliconf plugin - adjust function signature that was modified in Ansible after creation of this plugin (https://github.com/ansible-collections/community.routeros/pull/43). + +New Plugins +----------- + +Filter +~~~~~~ + +- community.routeros.join - Join a list of arguments to a command +- community.routeros.list_to_dict - Convert a list of arguments to a list of dictionary +- community.routeros.quote_argument - Quote an argument +- community.routeros.quote_argument_value - Quote an argument value +- community.routeros.split - Split a command into arguments v1.2.0 ====== @@ -74,7 +792,6 @@ Release Summary This is the first production (non-prerelease) release of ``community.routeros``. - Bugfixes -------- @@ -101,7 +818,6 @@ Release Summary The ``community.routeros`` continues the work on the Ansible RouterOS modules from their state in ``community.network`` 1.2.0. The changes listed here are thus relative to the modules ``community.network.routeros_*``. - Minor Changes ------------- diff --git a/CHANGELOG.rst.license b/CHANGELOG.rst.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/CHANGELOG.rst.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/LICENSES/BSD-2-Clause.txt b/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000..6810e04 --- /dev/null +++ b/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,8 @@ +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/LICENSES/GPL-3.0-or-later.txt b/LICENSES/GPL-3.0-or-later.txt new file mode 120000 index 0000000..012065c --- /dev/null +++ b/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1 @@ +../COPYING \ No newline at end of file diff --git a/README.md b/README.md index bbedf3c..7d4f4c3 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,44 @@ -# Community RouterOS Collection -[![CI](https://github.com/ansible-collections/community.routeros/workflows/CI/badge.svg?event=push)](https://github.com/ansible-collections/community.routeros/actions) [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.routeros)](https://codecov.io/gh/ansible-collections/community.routeros) + -Provides modules for [Ansible](https://www.ansible.com/community) to manage [MikroTik RouterOS](http://www.mikrotik-routeros.net/routeros.aspx) instances. +# Community RouterOS Collection +[![Documentation](https://img.shields.io/badge/docs-brightgreen.svg)](https://docs.ansible.com/ansible/devel/collections/community/routeros/) +[![CI](https://github.com/ansible-collections/community.routeros/actions/workflows/nox.yml/badge.svg?branch=main)](https://github.com/ansible-collections/community.routeros/actions) +[![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.routeros)](https://codecov.io/gh/ansible-collections/community.routeros) +[![REUSE status](https://api.reuse.software/badge/github.com/ansible-collections/community.routeros)](https://api.reuse.software/info/github.com/ansible-collections/community.routeros) + +Provides modules for [Ansible](https://www.ansible.com/community) to manage [MikroTik RouterOS](https://mikrotik.com/software) instances. You can find [documentation for the modules and plugins in this collection here](https://docs.ansible.com/ansible/devel/collections/community/routeros/). +## Code of Conduct + +We follow [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all our interactions within this project. + +If you encounter abusive behavior violating the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html), please refer to the [policy violations](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html#policy-violations) section of the Code of Conduct for information on how to raise a complaint. + +## Communication + +* Join the Ansible forum: + * [Get Help](https://forum.ansible.com/c/help/6): get help or help others.Please add appropriate tags if you start new discussions, for example the `routeros` tag. + * [Posts tagged with 'routeros'](https://forum.ansible.com/tag/routeros): subscribe to participate in RouterOS related conversations. + * [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts. + * [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events. + +* The Ansible [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn): used to announce releases and important changes. + +For more information about communication, see the [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html). + ## Tested with Ansible -Tested with the current Ansible 2.9, ansible-base 2.10 and ansible-core 2.11 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported. +Tested with the current ansible-core 2.15, ansible-core 2.16, ansible-core 2.17, ansible-core 2.18, and ansible-core 2.19 releases and the current development version of ansible-core. Ansible 2.9, ansible-base 2.10, and ansible-core versions before 2.15.0 are not supported. ## External requirements -The exact requirements for every module are listed in the module documentation. +The exact requirements for every module are listed in the module documentation. ### Supported connections @@ -21,9 +48,23 @@ The collection supports the `network_cli` connection. Please note that `community.routeros.api` module does **not** support Windows jump hosts! +## Collection Documentation + +Browsing the [**latest** collection documentation](https://docs.ansible.com/ansible/latest/collections/community/routeros) will show docs for the _latest version released in the Ansible package_, not the latest version of the collection released on Galaxy. + +Browsing the [**devel** collection documentation](https://docs.ansible.com/ansible/devel/collections/community/routeros) shows docs for the _latest version released on Galaxy_. + +We also separately publish [**latest commit** collection documentation](https://ansible-collections.github.io/community.routeros/branch/main/) which shows docs for the _latest commit in the `main` branch_. + +If you use the Ansible package and do not update collections independently, use **latest**. If you install or update this collection directly from Galaxy, use **devel**. If you are looking to contribute, use **latest commit**. + ## Included content - `community.routeros.api` +- `community.routeros.api_facts` +- `community.routeros.api_find_and_modify` +- `community.routeros.api_info` +- `community.routeros.api_modify` - `community.routeros.command` - `community.routeros.facts` @@ -69,19 +110,19 @@ Example playbook: hosts: routers gather_facts: false tasks: + - name: Run a command + community.routeros.command: + commands: + - /system resource print + register: system_resource_print + - name: Print its output + ansible.builtin.debug: + var: system_resource_print.stdout_lines - # Run a command and print its output - - community.routeros.command: - commands: - - /system resource print - register: system_resource_print - - debug: - var: system_resource_print.stdout_lines - - # Retrieve facts - - community.routeros.facts: - - debug: - msg: "First IP address: {{ ansible_net_all_ipv4_addresses[0] }}" + - name: Retrieve facts + community.routeros.facts: + - ansible.builtin.debug: + msg: "First IP address: {{ ansible_net_all_ipv4_addresses[0] }}" ``` ### Connecting with HTTP/HTTPS API @@ -92,23 +133,42 @@ Example playbook: --- - name: RouterOS test with API hosts: localhost - gather_facts: no + gather_facts: false vars: hostname: 192.168.1.1 username: admin password: test1234 + module_defaults: + group/community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + tls: true + force_no_cert: false + validate_certs: true + validate_cert_hostname: true + ca_path: /path/to/ca-certificate.pem tasks: - name: Get "ip address print" community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "ip address" - tls: true - validate_certs: true - validate_cert_hostname: true - ca_path: /path/to/ca-certificate.pem + path: ip address register: print_path + - name: Print the result + ansible.builtin.debug: + var: print_path.msg + + - name: Change IP address to 192.168.1.1 for interface bridge + community.routeros.api_find_and_modify: + path: ip address + find: + interface: bridge + values: + address: "192.168.1.1/24" + + - name: Retrieve facts + community.routeros.api_facts: + - ansible.builtin.debug: + msg: "First IP address: {{ ansible_net_all_ipv4_addresses[0] }}" ``` ## Contributing to this collection @@ -124,7 +184,7 @@ See [Ansible's dev guide](https://docs.ansible.com/ansible/devel/dev_guide/devel ## Release notes -See the [changelog](https://github.com/ansible-collections/community.routeros/blob/main/CHANGELOG.rst). +See the [collection's changelog](https://github.com/ansible-collections/community.routeros/blob/main/CHANGELOG.md). ## Roadmap @@ -142,6 +202,10 @@ We plan to regularly release minor and patch versions, whenever new features are ## Licensing -GNU General Public License v3.0 or later. +This collection is primarily licensed and distributed as a whole under the GNU General Public License v3.0 or later. -See [COPYING](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text. +See [LICENSES/GPL-3.0-or-later.txt](https://github.com/ansible-collections/community.routeros/blob/main/COPYING) for the full text. + +Parts of the collection are licensed under the [BSD 2-Clause license](https://github.com/ansible-collections/community.routeros/blob/main/LICENSES/BSD-2-Clause.txt). + +All files have a machine readable `SDPX-License-Identifier:` comment denoting its respective license(s) or an equivalent entry in an accompanying `.license` file. Only changelog fragments (which will not be part of a release) are covered by a blanket statement in `REUSE.toml`. This conforms to the [REUSE specification](https://reuse.software/spec/). diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..ff95bb8 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,11 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +version = 1 + +[[annotations]] +path = "changelogs/fragments/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "Ansible Project" +SPDX-License-Identifier = "GPL-3.0-or-later" diff --git a/antsibull-nox.toml b/antsibull-nox.toml new file mode 100644 index 0000000..72982fa --- /dev/null +++ b/antsibull-nox.toml @@ -0,0 +1,97 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +[collection_sources] +"community.internal_test_tools" = "git+https://github.com/ansible-collections/community.internal_test_tools.git,main" +"community.netcommon" = "git+https://github.com/ansible-collections/ansible.netcommon.git,main" +"community.utils" = "git+https://github.com/ansible-collections/ansible.utils.git,main" + +[sessions] + +[sessions.lint] +run_isort = false +run_black = false +run_flake8 = false +run_pylint = false +run_yamllint = true +yamllint_config = ".yamllint" +yamllint_config_plugins = ".yamllint-docs" +yamllint_config_plugins_examples = ".yamllint-examples" +yamllint_config_extra_docs = ".yamllint-extra-docs" +run_mypy = false + +[sessions.docs_check] +validate_collection_refs="all" +codeblocks_restrict_types = [ + "ansible-output", + "ini", + "yaml", + "yaml+jinja", +] +codeblocks_restrict_type_exact_case = true +codeblocks_allow_without_type = false +codeblocks_allow_literal_blocks = false + +[sessions.license_check] + +[sessions.extra_checks] +run_no_unwanted_files = true +no_unwanted_files_module_extensions = [".py"] +no_unwanted_files_yaml_extensions = [".yml"] +run_action_groups = true +run_no_trailing_whitespace = true +no_trailing_whitespace_skip_directories = [ + "tests/unit/plugins/modules/fixtures/", +] +run_avoid_characters = true + +[[sessions.extra_checks.action_groups_config]] +name = "api" +pattern = "^api.*$" +exclusions = [] +doc_fragment = "community.routeros.attributes.actiongroup_api" + +[[sessions.extra_checks.avoid_character_group]] +name = "tab" +regex = "\\x09" + +[sessions.build_import_check] +run_galaxy_importer = true + +[sessions.ansible_test_sanity] +include_devel = true + +[sessions.ansible_test_units] +include_devel = true + +[sessions.ansible_test_integration_w_default_container] +include_devel = true +controller_python_versions_only = true + +[sessions.ansible_test_integration_w_default_container.core_python_versions] +"2.15" = ["2.7", "3.6", "3.7"] +"2.16" = ["3.10"] +"2.17" = ["3.8"] +"2.18" = ["3.9"] +"2.19" = ["3.11"] + +[[sessions.ee_check.execution_environments]] +name = "devel-ubi-9" +description = "ansible-core devel @ RHEL UBI 9" +test_playbooks = ["tests/ee/all.yml"] +config.images.base_image.name = "docker.io/redhat/ubi9:latest" +config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/devel.tar.gz" +config.dependencies.ansible_runner.package_pip = "ansible-runner" +config.dependencies.python_interpreter.package_system = "python3.12 python3.12-pip python3.12-wheel python3.12-cryptography" +config.dependencies.python_interpreter.python_path = "/usr/bin/python3.12" +runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"} + +[[sessions.ee_check.execution_environments]] +name = "2.15-rocky-9" +description = "ansible-core 2.15 @ Rocky Linux 9" +test_playbooks = ["tests/ee/all.yml"] +config.images.base_image.name = "quay.io/rockylinux/rockylinux:9" +config.dependencies.ansible_core.package_pip = "https://github.com/ansible/ansible/archive/stable-2.15.tar.gz" +config.dependencies.ansible_runner.package_pip = "ansible-runner" +runtime_environment = {"ANSIBLE_PRIVATE_ROLE_VARS" = "true"} diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 0e41513..1ba1857 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -1,98 +1,973 @@ +--- ancestor: null releases: 0.1.0: changes: minor_changes: - - facts - now also collecting data about BGP and OSPF (https://github.com/ansible-collections/community.network/pull/101). - - facts - set configuration export on to verbose, for full configuration export - (https://github.com/ansible-collections/community.network/pull/104). + - facts - now also collecting data about BGP and OSPF (https://github.com/ansible-collections/community.network/pull/101). + - facts - set configuration export on to verbose, for full configuration export + (https://github.com/ansible-collections/community.network/pull/104). release_summary: 'The ``community.routeros`` continues the work on the Ansible RouterOS modules from their state in ``community.network`` 1.2.0. The changes listed here are thus relative to the modules ``community.network.routeros_*``. ' fragments: - - 0.1.0.yml - - 101_update_facts.yml - - 104_facts_export_verbose.yml + - 0.1.0.yml + - 101_update_facts.yml + - 104_facts_export_verbose.yml release_date: '2020-10-26' 0.1.1: changes: bugfixes: - - api - fix crash when the ``ssl`` parameter is used (https://github.com/ansible-collections/community.routeros/pull/3). + - api - fix crash when the ``ssl`` parameter is used (https://github.com/ansible-collections/community.routeros/pull/3). release_summary: Small improvements and bugfixes over the initial release. fragments: - - 0.1.1.yml - - 3-api-ssl.yml + - 0.1.1.yml + - 3-api-ssl.yml release_date: '2020-10-31' 1.0.0: changes: bugfixes: - - routeros terminal plugin - allow slashes in hostnames for terminal detection. - Without this, slashes in hostnames will result in connection timeouts (https://github.com/ansible-collections/community.network/pull/138). + - routeros terminal plugin - allow slashes in hostnames for terminal detection. + Without this, slashes in hostnames will result in connection timeouts (https://github.com/ansible-collections/community.network/pull/138). release_summary: 'This is the first production (non-prerelease) release of ``community.routeros``. ' fragments: - - 1.0.0.yml - - community.network-138-routeros-allow-slash.yml + - 1.0.0.yml + - community.network-138-routeros-allow-slash.yml release_date: '2020-11-17' 1.0.1: changes: bugfixes: - - api - remove ``id to .id`` as default requirement which conflicts with RouterOS - ``id`` configuration parameter (https://github.com/ansible-collections/community.routeros/pull/15). + - api - remove ``id to .id`` as default requirement which conflicts with RouterOS + ``id`` configuration parameter (https://github.com/ansible-collections/community.routeros/pull/15). release_summary: Maintenance release with a bugfix for ``api``. fragments: - - 1.0.1.yml - - 13-remove-id-restriction-for-api.yaml + - 1.0.1.yml + - 13-remove-id-restriction-for-api.yaml release_date: '2020-12-11' 1.1.0: changes: minor_changes: - - command - added support for a dash (``-``) in username (https://github.com/ansible-collections/community.routeros/pull/18). - - facts - added support for a dash (``-``) in username (https://github.com/ansible-collections/community.routeros/pull/18). + - command - added support for a dash (``-``) in username (https://github.com/ansible-collections/community.routeros/pull/18). + - facts - added support for a dash (``-``) in username (https://github.com/ansible-collections/community.routeros/pull/18). release_summary: This release allow dashes in usernames for SSH-based modules. fragments: - - 1.1.0.yml - - 18-support-dashes-in-username.yml + - 1.1.0.yml + - 18-support-dashes-in-username.yml release_date: '2021-01-04' 1.2.0: changes: bugfixes: - - api - when using TLS/SSL, remove explicit cipher configuration to insecure - values, which also makes it impossible to connect to newer RouterOS versions - (https://github.com/ansible-collections/community.routeros/pull/34). + - api - when using TLS/SSL, remove explicit cipher configuration to insecure + values, which also makes it impossible to connect to newer RouterOS versions + (https://github.com/ansible-collections/community.routeros/pull/34). minor_changes: - - Avoid internal ansible-core module_utils in favor of equivalent public API - available since at least Ansible 2.9 (https://github.com/ansible-collections/community.routeros/pull/38). - - api - add options ``validate_certs`` (default value ``true``), ``validate_cert_hostname`` - (default value ``false``), and ``ca_path`` to control certificate validation - (https://github.com/ansible-collections/community.routeros/pull/37). - - api - rename option ``ssl`` to ``tls``, and keep the old name as an alias - (https://github.com/ansible-collections/community.routeros/pull/37). - - fact - add fact ``ansible_net_config_nonverbose`` to get idempotent config - (no date, no verbose) (https://github.com/ansible-collections/community.routeros/pull/23). + - Avoid internal ansible-core module_utils in favor of equivalent public API + available since at least Ansible 2.9 (https://github.com/ansible-collections/community.routeros/pull/38). + - api - add options ``validate_certs`` (default value ``true``), ``validate_cert_hostname`` + (default value ``false``), and ``ca_path`` to control certificate validation + (https://github.com/ansible-collections/community.routeros/pull/37). + - api - rename option ``ssl`` to ``tls``, and keep the old name as an alias + (https://github.com/ansible-collections/community.routeros/pull/37). + - fact - add fact ``ansible_net_config_nonverbose`` to get idempotent config + (no date, no verbose) (https://github.com/ansible-collections/community.routeros/pull/23). release_summary: Bugfix and feature release. fragments: - - 1.2.0.yml - - 23-idempotent_config.yml - - 34-api-ciphers.yml - - 37-api-validate-cert-options.yml - - ansible-core-_text.yml + - 1.2.0.yml + - 23-idempotent_config.yml + - 34-api-ciphers.yml + - 37-api-validate-cert-options.yml + - ansible-core-_text.yml release_date: '2021-06-29' 2.0.0-a1: changes: breaking_changes: - - 'api - due to a programming error, the module never failed on errors. This - has now been fixed. If you are relying on the module not failing in case of - idempotent commands (resulting in errors like ``failure: already have such - address``), you need to adjust your roles/playbooks. We suggest to use ``failed_when`` - to accept failure in specific circumstances, for example ``failed_when: "''failure: - already have '' in result.msg[0]"`` (https://github.com/ansible-collections/community.routeros/pull/39).' + - 'api - due to a programming error, the module never failed on errors. This + has now been fixed. If you are relying on the module not failing in case + of idempotent commands (resulting in errors like ``failure: already have + such address``), you need to adjust your roles/playbooks. We suggest to + use ``failed_when`` to accept failure in specific circumstances, for example + ``failed_when: "''failure: already have '' in result.msg[0]"`` (https://github.com/ansible-collections/community.routeros/pull/39).' release_summary: First prerelease for a new major release with a breaking change in the behavior of ``community.routeros.api``. fragments: - - 2.0.0-a1.yml - - 39-api-fail.yml + - 2.0.0-a1.yml + - 39-api-fail.yml release_date: '2021-07-31' + 2.0.0-a2: + changes: + breaking_changes: + - api - splitting commands no longer uses a naive split by whitespace, but + a more RouterOS CLI compatible splitting algorithm (https://github.com/ansible-collections/community.routeros/pull/45). + - command - the module now always indicates that a change happens. If this + is not correct, please use ``changed_when`` to determine the correct changed + status for a task (https://github.com/ansible-collections/community.routeros/pull/50). + bugfixes: + - api - improve splitting of ``WHERE`` queries (https://github.com/ansible-collections/community.routeros/pull/47). + - api - when converting result lists to dictionaries, no longer removes second + ``=`` and text following that if present (https://github.com/ansible-collections/community.routeros/pull/47). + - routeros cliconf plugin - adjust function signature that was modified in + Ansible after creation of this plugin (https://github.com/ansible-collections/community.routeros/pull/43). + minor_changes: + - api - make validation of ``WHERE`` for ``query`` more strict (https://github.com/ansible-collections/community.routeros/pull/53). + release_summary: Second prerelease for a new major release with breaking changes + in the behavior of ``community.routeros.api`` and ``community.routeros.command``. + fragments: + - 2.0.0-a2.yml + - 43-sanity.yml + - 45-api-split.yml + - 47-api-split.yml + - 50-command-changed.yml + - 53-api-where.yml + - 53-quoting-filters.yml + plugins: + filter: + - description: Join a list of arguments to a command + name: join + namespace: null + - description: Convert a list of arguments to a list of dictionary + name: list_to_dict + namespace: null + - description: Quote an argument + name: quote_argument + namespace: null + - description: Quote an argument value + name: quote_argument_value + namespace: null + - description: Split a command into arguments + name: split + namespace: null + release_date: '2021-10-14' + 2.0.0: + changes: + minor_changes: + - command - the ``commands`` and ``wait_for`` options now convert the list + elements to strings (https://github.com/ansible-collections/community.routeros/pull/55). + - facts - the ``gather_subset`` option now converts the list elements to strings + (https://github.com/ansible-collections/community.routeros/pull/55). + release_summary: A new major release with breaking changes in the behavior of + ``community.routeros.api`` and ``community.routeros.command``. + fragments: + - 2.0.0.yml + - 55-linting.yml + release_date: '2021-10-31' + 2.1.0: + changes: + bugfixes: + - query - fix query function check for ``.id`` vs. ``id`` arguments to not + conflict with routeros arguments like ``identity`` (https://github.com/ansible-collections/community.routeros/pull/68, + https://github.com/ansible-collections/community.routeros/issues/67). + - quoting and unquoting filter plugins, api module - handle the escape sequence + ``\_`` correctly as escaping a space and not an underscore (https://github.com/ansible-collections/community.routeros/pull/89). + minor_changes: + - Added a ``community.routeros.api`` module defaults group. Use with ``group/community.routeros.api`` + to provide options for all API-based modules (https://github.com/ansible-collections/community.routeros/pull/89). + - Prepare collection for inclusion in an Execution Environment by declaring + its dependencies (https://github.com/ansible-collections/community.routeros/pull/83). + - api - add new option ``extended query`` more complex queries against RouterOS + API (https://github.com/ansible-collections/community.routeros/pull/63). + - api - update ``query`` to accept symbolic parameters (https://github.com/ansible-collections/community.routeros/pull/63). + - api* modules - allow to set an encoding other than the default ASCII for + communicating with the API (https://github.com/ansible-collections/community.routeros/pull/95). + release_summary: Feature and bugfix release with new modules. + fragments: + - 2.1.0.yml + - 63-add-extended_query.yml + - 68-fix-query-id-check.yml + - 83-ee.yml + - 89-quoting.yml + - 90-api-action-group.yml + - 95-api-encoding.yml + modules: + - description: Collect facts from remote devices running MikroTik RouterOS using + the API + name: api_facts + namespace: '' + - description: Find and modify information using the API + name: api_find_and_modify + namespace: '' + release_date: '2022-05-25' + 2.2.0: + changes: + bugfixes: + - Include ``LICENSES/BSD-2-Clause.txt`` file for the ``routeros`` module utils + (https://github.com/ansible-collections/community.routeros/pull/101). + minor_changes: + - All software licenses are now in the ``LICENSES/`` directory of the collection + root. Moreover, ``SPDX-License-Identifier:`` is used to declare the applicable + license for every file that is not automatically generated (https://github.com/ansible-collections/community.routeros/pull/101). + release_summary: New feature release. + fragments: + - 101-licenses.yml + - 2.2.0.yml + modules: + - description: Retrieve information from API + name: api_info + namespace: '' + - description: Modify data at paths with API + name: api_modify + namespace: '' + release_date: '2022-07-31' + 2.2.1: + changes: + bugfixes: + - api_modify, api_info - make API path ``ip dhcp-server lease`` support ``server=all`` + (https://github.com/ansible-collections/community.routeros/issues/104, https://github.com/ansible-collections/community.routeros/pull/107). + - api_modify, api_info - make API path ``ip dhcp-server network`` support + missing options ``boot-file-name``, ``dhcp-option-set``, ``dns-none``, ``domain``, + and ``next-server`` (https://github.com/ansible-collections/community.routeros/issues/104, + https://github.com/ansible-collections/community.routeros/pull/106). + release_summary: Bugfix release. + fragments: + - 106-api-path-ip-dhcp-network.yml + - 107-api-path-ip-dhcp-lease.yml + - 2.2.1.yml + release_date: '2022-08-20' + 2.3.0: + changes: + bugfixes: + - api_modify, api_info - make API path ``ip dhcp-server`` support ``script``, + and ``ip firewall nat`` support ``in-interface`` and ``in-interface-list`` + (https://github.com/ansible-collections/community.routeros/pull/110). + minor_changes: + - The collection repository conforms to the `REUSE specification `__ + except for the changelog fragments (https://github.com/ansible-collections/community.routeros/pull/108). + - api* modules - added ``timeout`` parameter (https://github.com/ansible-collections/community.routeros/pull/109). + - api_modify, api_info - support API path ``ip firewall mangle`` (https://github.com/ansible-collections/community.routeros/pull/110). + release_summary: Feature and bugfix release. + fragments: + - 109-add-timeout-parameter-to-api.yml + - 110-api.yml + - 2.3.0.yml + - licenses.yml + release_date: '2022-09-11' + 2.3.1: + changes: + known_issues: + - The ``community.routeros.command`` module claims to support check mode. + Since it cannot judge whether the commands executed modify state or not, + this behavior is incorrect. Since this potentially breaks existing playbooks, + we will not change this behavior until community.routeros 3.0.0. + release_summary: Maintenance release with improved documentation. + fragments: + - 2.3.1.yml + - command-check_mode.yml + release_date: '2022-11-06' + 2.4.0: + changes: + bugfixes: + - api_modify - ``ip route`` entry can be defined without the need of ``gateway`` + field, which is correct for unreachable/blackhole type of routes (https://github.com/ansible-collections/community.routeros/pull/131). + - api_modify - ``queue interface`` path works now (https://github.com/ansible-collections/community.routeros/pull/131). + - api_modify, api_info - removed wrong field ``dynamic`` from API path ``ipv6 + firewall address-list`` (https://github.com/ansible-collections/community.routeros/pull/133). + - api_modify, api_info - the default of the field ``ingress-filtering`` in + ``interface bridge port`` is now ``true``, which is the default in ROS (https://github.com/ansible-collections/community.routeros/pull/125). + - command, facts - commands do not timeout in safe mode anymore (https://github.com/ansible-collections/community.routeros/pull/134). + known_issues: + - api_modify - when limits for entries in ``queue tree`` are defined as human + readable - for example ``25M`` -, the configuration will be correctly set + in ROS, but the module will indicate the item is changed on every run even + when there was no change done. This is caused by the ROS API which returns + the number in bytes - for example ``25000000`` (which is inconsistent with + the CLI behavior). In order to mitigate that, the limits have to be defined + in bytes (those will still appear as human readable in the ROS CLI) (https://github.com/ansible-collections/community.routeros/pull/131). + - api_modify, api_info - ``routing ospf area``, ``routing ospf area range``, + ``routing ospf instance``, ``routing ospf interface-template`` paths are + not fully implemented for ROS6 due to the significant changes between ROS6 + and ROS7 (https://github.com/ansible-collections/community.routeros/pull/131). + minor_changes: + - api* modules - Add new option ``force_no_cert`` to connect with ADH ciphers + (https://github.com/ansible-collections/community.routeros/pull/124). + - api_info - new parameter ``include_builtin`` which allows to include "builtin" + entries that are automatically generated by ROS and cannot be modified by + the user (https://github.com/ansible-collections/community.routeros/pull/130). + - api_modify, api_info - support API paths - ``interface bonding``, ``interface + bridge mlag``, ``ipv6 firewall mangle``, ``ipv6 nd``, ``system scheduler``, + ``system script``, ``system ups`` (https://github.com/ansible-collections/community.routeros/pull/133). + - api_modify, api_info - support API paths ``caps-man access-list``, ``caps-man + configuration``, ``caps-man datapath``, ``caps-man manager``, ``caps-man + provisioning``, ``caps-man security`` (https://github.com/ansible-collections/community.routeros/pull/126). + - api_modify, api_info - support API paths ``interface list`` and ``interface + list member`` (https://github.com/ansible-collections/community.routeros/pull/120). + - api_modify, api_info - support API paths ``interface pppoe-client``, ``interface + vlan``, ``interface bridge``, ``interface bridge vlan`` (https://github.com/ansible-collections/community.routeros/pull/125). + - api_modify, api_info - support API paths ``ip ipsec identity``, ``ip ipsec + peer``, ``ip ipsec policy``, ``ip ipsec profile``, ``ip ipsec proposal`` + (https://github.com/ansible-collections/community.routeros/pull/129). + - api_modify, api_info - support API paths ``ip route`` and ``ip route vrf`` + (https://github.com/ansible-collections/community.routeros/pull/123). + - api_modify, api_info - support API paths ``ipv6 address``, ``ipv6 dhcp-server``, + ``ipv6 dhcp-server option``, ``ipv6 route``, ``queue tree``, ``routing ospf + area``, ``routing ospf area range``, ``routing ospf instance``, ``routing + ospf interface-template``, ``routing pimsm instance``, ``routing pimsm interface-template`` + (https://github.com/ansible-collections/community.routeros/pull/131). + - api_modify, api_info - support API paths ``system logging``, ``system logging + action`` (https://github.com/ansible-collections/community.routeros/pull/127). + - api_modify, api_info - support field ``hw-offload`` for path ``ip firewall + filter`` (https://github.com/ansible-collections/community.routeros/pull/121). + - api_modify, api_info - support fields ``address-list``, ``address-list-timeout``, + ``connection-bytes``, ``connection-limit``, ``connection-mark``, ``connection-rate``, + ``connection-type``, ``content``, ``disabled``, ``dscp``, ``dst-address-list``, + ``dst-address-type``, ``dst-limit``, ``fragment``, ``hotspot``, ``icmp-options``, + ``in-bridge-port``, ``in-bridge-port-list``, ``ingress-priority``, ``ipsec-policy``, + ``ipv4-options``, ``jump-target``, ``layer7-protocol``, ``limit``, ``log``, + ``log-prefix``, ``nth``, ``out-bridge-port``, ``out-bridge-port-list``, + ``packet-mark``, ``packet-size``, ``per-connection-classifier``, ``port``, + ``priority``, ``psd``, ``random``, ``realm``, ``routing-mark``, ``same-not-by-dst``, + ``src-address``, ``src-address-list``, ``src-address-type``, ``src-mac-address``, + ``src-port``, ``tcp-mss``, ``time``, ``tls-host``, ``ttl`` in ``ip firewall + nat`` path (https://github.com/ansible-collections/community.routeros/pull/133). + - api_modify, api_info - support fields ``combo-mode``, ``comment``, ``fec-mode``, + ``mdix-enable``, ``poe-out``, ``poe-priority``, ``poe-voltage``, ``power-cycle-interval``, + ``power-cycle-ping-address``, ``power-cycle-ping-enabled``, ``power-cycle-ping-timeout`` + for path ``interface ethernet`` (https://github.com/ansible-collections/community.routeros/pull/121). + - api_modify, api_info - support fields ``jump-target``, ``reject-with`` in + ``ip firewall filter`` API path, field ``comment`` in ``ip firwall address-list`` + API path, field ``jump-target`` in ``ip firewall mangle`` API path, field + ``comment`` in ``ipv6 firewall address-list`` API path, fields ``jump-target``, + ``reject-with`` in ``ipv6 firewall filter`` API path (https://github.com/ansible-collections/community.routeros/pull/133). + - api_modify, api_info - support for API fields that can be disabled and have + default value at the same time, support API paths ``interface gre``, ``interface + eoip`` (https://github.com/ansible-collections/community.routeros/pull/128). + - api_modify, api_info - support for fields ``blackhole``, ``pref-src``, ``routing-table``, + ``suppress-hw-offload``, ``type``, ``vrf-interface`` in ``ip route`` path + (https://github.com/ansible-collections/community.routeros/pull/131). + - api_modify, api_info - support paths ``system ntp client servers`` and ``system + ntp server`` available in ROS7, as well as new fields ``servers``, ``mode``, + and ``vrf`` for ``system ntp client`` (https://github.com/ansible-collections/community.routeros/pull/122). + release_summary: Feature release improving the ``api*`` modules. + fragments: + - 120-api.yml + - 121-api.yml + - 122-api.yml + - 123-api.yml + - 124-api.yml + - 125-api.yml + - 126-api-capsman.yml + - 127-logging.yml + - 128-api.yml + - 129-api-ipsec.yml + - 130-api-modify-builtin.yml + - 131-api.yml + - 133-api.yml + - 134-command-safemode.yml + - 2.4.0.yml + release_date: '2022-11-18' + 2.5.0: + changes: + bugfixes: + - api_modify - ``address-pool`` field of entries in API path ``ip dhcp-server`` + is not required anymore (https://github.com/ansible-collections/community.routeros/pull/137). + minor_changes: + - api_info, api_modify - support API paths ``interface ethernet poe``, ``interface + gre6``, ``interface vrrp`` and also support all previously missing fields + of entries in ``ip dhcp-server`` (https://github.com/ansible-collections/community.routeros/pull/137). + release_summary: Feature and bugfix release. + fragments: + - 137-api.yml + - 2.5.0.yml + release_date: '2022-12-04' + 2.6.0: + changes: + bugfixes: + - api_modify - do not use ``name`` as a unique key in ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). + - api_modify, api_info - do not crash if router contains ``regexp`` DNS entries + in ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). + minor_changes: + - api_modify, api_info - add field ``regexp`` to ``ip dns static`` (https://github.com/ansible-collections/community.routeros/issues/141). + - api_modify, api_info - support API paths ``interface wireguard``, ``interface + wireguard peers`` (https://github.com/ansible-collections/community.routeros/pull/143). + release_summary: Regular bugfix and feature release. + fragments: + - 142-dns-regexp.yml + - 143-add-wireguard.yml + - 2.6.0.yml + release_date: '2023-01-01' + 2.7.0: + changes: + bugfixes: + - api_modify, api_info - defaults corrected for fields in ``interface wireguard + peers`` API path (https://github.com/ansible-collections/community.routeros/pull/144). + minor_changes: + - api_modify, api_info - support API paths ``ip arp``, ``ip firewall raw``, + ``ipv6 firewall raw`` (https://github.com/ansible-collections/community.routeros/pull/144). + release_summary: Bugfix and feature release. + fragments: + - 144-paths.yml + - 2.7.0.yml + release_date: '2023-01-14' + 2.8.0: + changes: + bugfixes: + - api_info, api_modify - fix default and remove behavior for ``dhcp-options`` + in path ``ip dhcp-client`` (https://github.com/ansible-collections/community.routeros/issues/148, + https://github.com/ansible-collections/community.routeros/pull/154). + - api_modify - fix handling of disabled keys on creation (https://github.com/ansible-collections/community.routeros/pull/154). + - various plugins and modules - remove unnecessary imports (https://github.com/ansible-collections/community.routeros/pull/149). + minor_changes: + - api_modify - adapt data for API paths ``ip dhcp-server network`` (https://github.com/ansible-collections/community.routeros/pull/156). + - api_modify - add support for API path ``snmp community`` (https://github.com/ansible-collections/community.routeros/pull/159). + - api_modify - add support for ``trap-interfaces`` in API path ``snmp`` (https://github.com/ansible-collections/community.routeros/pull/159). + - api_modify - add support to disable IPv6 in API paths ``ipv6 settings`` + (https://github.com/ansible-collections/community.routeros/pull/158). + - api_modify - support API paths ``ip firewall layer7-protocol`` (https://github.com/ansible-collections/community.routeros/pull/153). + - command - workaround for extra characters in stdout in RouterOS versions + between 6.49 and 7.1.5 (https://github.com/ansible-collections/community.routeros/issues/62, + https://github.com/ansible-collections/community.routeros/pull/161). + release_summary: Bugfix and feature release. + fragments: + - 153-ip_firewall_layer7-protocol.yml + - 154-ip-dhcp-client-dhcp-options.yml + - 156-ip_dhcp-server_network.yml + - 158-ipv6_settings-disable.yml + - 159-snmp_community.yml + - 161-workaround-prompt-with-space.yml + - 2.8.0.yml + - remove-unneeded-imports.yml + release_date: '2023-03-23' + 2.8.1: + changes: + bugfixes: + - facts - do not crash in CLI output preprocessing in unexpected situations + during line unwrapping (https://github.com/ansible-collections/community.routeros/issues/170, + https://github.com/ansible-collections/community.routeros/pull/177). + release_summary: Bugfix release. + fragments: + - 177-facts-parsing.yml + - 2.8.1.yml + release_date: '2023-06-14' + 2.8.2: + changes: + bugfixes: + - api_modify, api_info - add missing parameter ``tls`` for the ``tool e-mail`` + path (https://github.com/ansible-collections/community.routeros/issues/179, + https://github.com/ansible-collections/community.routeros/pull/180). + release_summary: Bugfix release. + fragments: + - 180-fix-tls-in-tool-email.yml + - 2.8.2.yml + release_date: '2023-06-19' + 2.8.3: + changes: + known_issues: + - Ansible markup will show up in raw form on ansible-doc text output for ansible-core + before 2.15. If you have trouble deciphering the documentation markup, please + upgrade to ansible-core 2.15 (or newer), or read the HTML documentation + on https://docs.ansible.com/ansible/devel/collections/community/routeros/. + release_summary: 'Maintenance release with updated documentation. + + + From this version on, community.routeros is using the new `Ansible semantic + markup + + `__ + + in its documentation. If you look at documentation with the ansible-doc CLI + tool + + from ansible-core before 2.15, please note that it does not render the markup + + correctly. You should be still able to read it in most cases, but you need + + ansible-core 2.15 or later to see it as it is intended. Alternatively you + can + + look at `the devel docsite `__ + + for the rendered HTML version of the documentation of the latest release. + + ' + fragments: + - 2.8.3.yml + - semantic-markup.yml + release_date: '2023-06-27' + 2.9.0: + changes: + bugfixes: + - api_modify, api_info - add missing parameter ``engine-id-suffix`` for the + ``snmp`` path (https://github.com/ansible-collections/community.routeros/issues/189, + https://github.com/ansible-collections/community.routeros/pull/190). + minor_changes: + - api_info, api_modify - add path ``caps-man channel`` and enable path ``caps-man + manager interface`` (https://github.com/ansible-collections/community.routeros/issues/193, + https://github.com/ansible-collections/community.routeros/pull/194). + - api_info, api_modify - add path ``ip traffic-flow target`` (https://github.com/ansible-collections/community.routeros/issues/191, + https://github.com/ansible-collections/community.routeros/pull/192). + release_summary: Bugfix and feature release. + fragments: + - 180-fix-engine-id-suffix-in-snmp.yml + - 192-add-ip_traffic-flow_target-path.yml + - 194-add-caps-man_channel-and-caps-man_manager_interface.yml + - 2.9.0.yml + release_date: '2023-08-15' + 2.10.0: + changes: + bugfixes: + - api_info, api_modify - in the ``snmp`` path, ensure that ``engine-id-suffix`` + is only available on RouterOS 7.10+, and that ``engine-id`` is read-only + on RouterOS 7.10+ (https://github.com/ansible-collections/community.routeros/issues/208, + https://github.com/ansible-collections/community.routeros/pull/218). + minor_changes: + - api_info - add new ``include_read_only`` option to select behavior for read-only + values. By default these are not returned (https://github.com/ansible-collections/community.routeros/pull/213). + - api_info, api_modify - add support for ``address-list`` and ``match-subdomain`` + introduced by RouterOS 7.7 in the ``ip dns static`` path (https://github.com/ansible-collections/community.routeros/pull/197). + - api_info, api_modify - add support for ``user``, ``time`` and ``gmt-offset`` + under the ``system clock`` path (https://github.com/ansible-collections/community.routeros/pull/210). + - api_info, api_modify - add support for the ``interface ppp-client`` path + (https://github.com/ansible-collections/community.routeros/pull/199). + - api_info, api_modify - add support for the ``interface wireless`` path (https://github.com/ansible-collections/community.routeros/pull/195). + - api_info, api_modify - add support for the ``iot modbus`` path (https://github.com/ansible-collections/community.routeros/pull/205). + - api_info, api_modify - add support for the ``ip dhcp-server option`` and + ``ip dhcp-server option sets`` paths (https://github.com/ansible-collections/community.routeros/pull/223). + - api_info, api_modify - add support for the ``ip upnp interfaces``, ``tool + graphing interface``, ``tool graphing resource`` paths (https://github.com/ansible-collections/community.routeros/pull/227). + - api_info, api_modify - add support for the ``ipv6 firewall nat`` path (https://github.com/ansible-collections/community.routeros/pull/204). + - api_info, api_modify - add support for the ``mode`` property in ``ip neighbor + discovery-settings`` introduced in RouterOS 7.7 (https://github.com/ansible-collections/community.routeros/pull/198). + - api_info, api_modify - add support for the ``port remote-access`` path (https://github.com/ansible-collections/community.routeros/pull/224). + - api_info, api_modify - add support for the ``routing filter rule`` and ``routing + filter select-rule`` paths (https://github.com/ansible-collections/community.routeros/pull/200). + - api_info, api_modify - add support for the ``routing table`` path in RouterOS + 7 (https://github.com/ansible-collections/community.routeros/pull/215). + - api_info, api_modify - add support for the ``tool netwatch`` path in RouterOS + 7 (https://github.com/ansible-collections/community.routeros/pull/216). + - api_info, api_modify - add support for the ``user settings`` path (https://github.com/ansible-collections/community.routeros/pull/201). + - api_info, api_modify - add support for the ``user`` path (https://github.com/ansible-collections/community.routeros/pull/211). + - api_info, api_modify - finalize fields for the ``interface wireless security-profiles`` + path and enable it (https://github.com/ansible-collections/community.routeros/pull/203). + - api_info, api_modify - finalize fields for the ``ppp profile`` path and + enable it (https://github.com/ansible-collections/community.routeros/pull/217). + - api_modify - add new ``handle_read_only`` and ``handle_write_only`` options + to handle the module's behavior for read-only and write-only fields (https://github.com/ansible-collections/community.routeros/pull/213). + - api_modify, api_info - support API paths ``routing id``, ``routing bgp connection`` + (https://github.com/ansible-collections/community.routeros/pull/220). + release_summary: Bugfix and feature release. + fragments: + - 195-add-interface-wireless-data.yml + - 197-dns-static-addrlist-matchsubdomain.yml + - 198-ip-nd-mode.yml + - 199-add-interface-pppclient.yml + - 2.10.0.yml + - 200-add-routing-filter.yml + - 201-add-user-settings.yml + - 203-wireless-security-profiles.yml + - 204-add-ipv6-firewall-nat.yml + - 205-add-iot-modbus.yml + - 210-date-time-gmt-offset.yml + - 211-user.yml + - 213-read-write-only.yml + - 215-add-routing-table.yml + - 216-add-tool-netwatch.yml + - 217-ppp-profiles.yml + - 218-snmp-engine-id.yml + - 220-routing-id-bgp-connection.yml + - 223-add-ip-dhcp-server-option.yml + - 224-add-port-remote-access.yml + - 227-add-upnp-graphing.yml + release_date: '2023-10-08' + 2.11.0: + changes: + minor_changes: + - api_info, api_modify - add missing DoH parameters ``doh-max-concurrent-queries``, + ``doh-max-server-connections``, and ``doh-timeout`` to the ``ip dns`` path + (https://github.com/ansible-collections/community.routeros/issues/230, https://github.com/ansible-collections/community.routeros/pull/235) + - api_info, api_modify - add missing parameters ``address-list``, ``address-list-timeout``, + ``randomise-ports``, and ``realm`` to subpaths of the ``ip firewall`` path + (https://github.com/ansible-collections/community.routeros/issues/236, https://github.com/ansible-collections/community.routeros/pull/237). + - api_info, api_modify - mark the ``interface wireless`` parameter ``running`` + as read-only (https://github.com/ansible-collections/community.routeros/pull/233). + - api_info, api_modify - set the default value to ``false`` for the ``disabled`` + parameter in some more paths where it can be seen in the documentation (https://github.com/ansible-collections/community.routeros/pull/237). + - api_modify - add missing ``comment`` attribute to ``/routing id`` (https://github.com/ansible-collections/community.routeros/pull/234). + - api_modify - add missing attributes to the ``routing bgp connection`` path + (https://github.com/ansible-collections/community.routeros/pull/234). + - api_modify - add versioning to the ``/tool e-mail`` path (RouterOS 7.12 + release) (https://github.com/ansible-collections/community.routeros/pull/234). + - api_modify - make ``/ip traffic-flow target`` a multiple value attribute + (https://github.com/ansible-collections/community.routeros/pull/234). + release_summary: Feature and bugfix release. + fragments: + - 2.11.0.yml + - 233-wireless-running-read-only.yml + - 234-bugfixes-and-update-adaptations.yml + - 235-add-missing-dns-attributes.yml + - 237-add-missing-ip-firewall-attributes.yml + release_date: '2023-12-03' + 2.12.0: + changes: + minor_changes: + - api_info, api_modify - add ``interface ovpn-client`` path (https://github.com/ansible-collections/community.routeros/issues/242, + https://github.com/ansible-collections/community.routeros/pull/244). + - api_info, api_modify - add ``radius`` path (https://github.com/ansible-collections/community.routeros/issues/241, + https://github.com/ansible-collections/community.routeros/pull/245). + - api_info, api_modify - add ``routing rule`` path (https://github.com/ansible-collections/community.routeros/issues/162, + https://github.com/ansible-collections/community.routeros/pull/246). + - api_info, api_modify - add missing path ``routing bgp template`` (https://github.com/ansible-collections/community.routeros/pull/243). + - api_info, api_modify - add support for the ``tx-power`` attribute in ``interface + wireless`` (https://github.com/ansible-collections/community.routeros/pull/239). + - api_info, api_modify - removed ``host`` primary key in ``tool netwatch`` + path (https://github.com/ansible-collections/community.routeros/pull/248). + - api_modify, api_info - added support for ``interface wifiwave2`` (https://github.com/ansible-collections/community.routeros/pull/226). + release_summary: Feature release. + fragments: + - 2.12.0.yml + - 226-support-for-WifiWave2.yml + - 239-wireless-tx-power.yml + - 243-add-routing-bgp-template-path.yml + - 244-add-interface-ovpn-client-path.yml + - 245-add-radius-path.yml + - 246-add-routing-rule-path.yml + - 247-removed-primary-key-host-in-tool-netwatch.yml + release_date: '2024-01-21' + 2.13.0: + changes: + bugfixes: + - facts - fix date not getting removed for idempotent config export (https://github.com/ansible-collections/community.routeros/pull/262). + minor_changes: + - api_info, api_modify - make path ``user group`` modifiable and add ``comment`` + attribute (https://github.com/ansible-collections/community.routeros/issues/256, + https://github.com/ansible-collections/community.routeros/pull/257). + - api_modify, api_info - add support for the ``ip vrf`` path in RouterOS 7 (https://github.com/ansible-collections/community.routeros/pull/259) + release_summary: Bugfix and feature release. + fragments: + - 2.13.0.yml + - 257-make-user_group-modifiable.yml + - 259-add-routeros7-support-for-ip-vrf.yml + - 262-fix-date-removal.yml + release_date: '2024-02-25' + 2.14.0: + changes: + minor_changes: + - api_info, api_modify - add read-only fields ``installed-version``, ``latest-version`` + and ``status`` in ``system package update`` (https://github.com/ansible-collections/community.routeros/pull/263). + - api_info, api_modify - added support for ``interface wifi`` and its sub-paths + (https://github.com/ansible-collections/community.routeros/pull/266). + - api_info, api_modify - remove default value for read-only ``running`` field + in ``interface wireless`` (https://github.com/ansible-collections/community.routeros/pull/264). + release_summary: Feature release. + fragments: + - 2.14.0.yml + - 263-sys-pkg-update.yml + - 264-wireless-running-default.yml + - 266-interface-wifi.yml + release_date: '2024-03-25' + 2.15.0: + changes: + minor_changes: + - api_info, api_modify - Add RouterOS 7.x support to ``/mpls ldp`` path (https://github.com/ansible-collections/community.routeros/pull/271). + - api_info, api_modify - add ``/ip route rule`` path for RouterOS 6.x (https://github.com/ansible-collections/community.routeros/pull/278). + - api_info, api_modify - add ``/routing filter`` path for RouterOS 6.x (https://github.com/ansible-collections/community.routeros/pull/279). + - api_info, api_modify - add default value for ``from-pool`` field in ``/ipv6 + address`` (https://github.com/ansible-collections/community.routeros/pull/270). + - api_info, api_modify - add missing path ``/interface pppoe-server server`` + (https://github.com/ansible-collections/community.routeros/pull/273). + - api_info, api_modify - add missing path ``/ip dhcp-relay`` (https://github.com/ansible-collections/community.routeros/pull/276). + - api_info, api_modify - add missing path ``/queue simple`` (https://github.com/ansible-collections/community.routeros/pull/269). + - api_info, api_modify - add missing path ``/queue type`` (https://github.com/ansible-collections/community.routeros/pull/274). + - api_info, api_modify - add missing paths ``/routing bgp aggregate``, ``/routing + bgp network`` and ``/routing bgp peer`` (https://github.com/ansible-collections/community.routeros/pull/277). + - api_info, api_modify - add support for paths ``/mpls interface``, ``/mpls + ldp accept-filter``, ``/mpls ldp advertise-filter`` and ``mpls ldp interface`` + (https://github.com/ansible-collections/community.routeros/pull/272). + release_summary: Feature release. + fragments: + - 2.15.0.yml + - 269-add-queue_simple-path.yml + - 270_fix_ipv6_from_pool_default_value.yml + - 271-mpls_ldp_routeros_7_support.yml + - 272-additional_mpls_path_support.yml + - 273-add_interface_pppoe-server_support.yml + - 274-add_queue_type_path.yml + - 276-add_ip_dhcp-relay_path.yml + - 277-add_routing_bgp_paths.yml + - 278-add_ip_route_rule_path.yml + - 279-add_routing_filter_path.yml + release_date: '2024-04-20' + 2.16.0: + changes: + minor_changes: + - api_info, api_modify - add missing path ``/ppp secret`` (https://github.com/ansible-collections/community.routeros/pull/286). + - api_info, api_modify - minor changes ``/interface ethernet`` path fields + (https://github.com/ansible-collections/community.routeros/pull/288). + release_summary: Feature release. + fragments: + - 2.16.0.yml + - 286-add_ppp_secret_path.yml + - 288-interface_ethernet_values.yml + release_date: '2024-06-16' + 2.17.0: + changes: + minor_changes: + - api_info, api_modify - add ``system health settings`` path (https://github.com/ansible-collections/community.routeros/pull/294). + - api_info, api_modify - add missing path ``/system resource irq rps`` (https://github.com/ansible-collections/community.routeros/pull/295). + - api_info, api_modify - add parameter ``host-key-type`` for ``ip ssh`` path + (https://github.com/ansible-collections/community.routeros/issues/280, https://github.com/ansible-collections/community.routeros/pull/297). + release_summary: Feature release. + fragments: + - 2.17.0.yml + - 294-add-system-health-settings-path.yml + - 295-add_system_resource_irq_rps_path.yml + - 297-add-ip-ssh-host-key-type.yml + release_date: '2024-07-09' + 2.18.0: + changes: + bugfixes: + - api_modify, api_info - change the default of ``ingress-filtering`` in paths + ``interface bridge`` and ``interface bridge port`` back to ``false`` for + RouterOS before version 7 (https://github.com/ansible-collections/community.routeros/pull/305). + deprecated_features: + - The collection deprecates support for all Ansible/ansible-base/ansible-core + versions that are currently End of Life, `according to the ansible-core + support matrix `__. + This means that the next major release of the collection will no longer + support Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core + 2.12, ansible-core 2.13, and ansible-core 2.14. + minor_changes: + - api_info - allow to restrict the output by limiting fields to specific values + with the new ``restrict`` option (https://github.com/ansible-collections/community.routeros/pull/305). + - api_info, api_modify - add support for the ``ip dhcp-server matcher`` path + (https://github.com/ansible-collections/community.routeros/pull/300). + - api_info, api_modify - add support for the ``ipv6 nd prefix`` path (https://github.com/ansible-collections/community.routeros/pull/303). + - api_info, api_modify - add support for the ``name`` and ``is-responder`` + properties under the ``interface wireguard peers`` path introduced in RouterOS + 7.15 (https://github.com/ansible-collections/community.routeros/pull/304). + - api_info, api_modify - add support for the ``routing ospf static-neighbor`` + path in RouterOS 7 (https://github.com/ansible-collections/community.routeros/pull/302). + - api_info, api_modify - set default for ``force`` in ``ip dhcp-server option`` + to an explicit ``false`` (https://github.com/ansible-collections/community.routeros/pull/300). + - api_modify - allow to restrict what is updated by limiting fields to specific + values with the new ``restrict`` option (https://github.com/ansible-collections/community.routeros/pull/305). + release_summary: Feature release. + fragments: + - 2.18.0.yml + - 300-add-ip-dhcp-server-matcher.yml + - 300-set-ip-dhcp-option-force-default.yml + - 302-ospf-static-neighbor.yml + - 303-add-ipv6-nd-prefix.yml + - 304-wireguard-name-responder.yml + - 305-api-restrict.yml + - 306-ingress-filtering-ros6.yml + - deprecate-eol-ansible-core.yml + release_date: '2024-08-12' + 2.19.0: + changes: + minor_changes: + - api_info, api_modify - add support for the ``ip dns adlist`` path implemented + by RouterOS 7.15 and newer (https://github.com/ansible-collections/community.routeros/pull/310). + - api_info, api_modify - add support for the ``mld-version`` and ``multicast-querier`` + properties in ``interface bridge`` (https://github.com/ansible-collections/community.routeros/pull/315). + - api_info, api_modify - add support for the ``routing filter num-list`` path + implemented by RouterOS 7 and newer (https://github.com/ansible-collections/community.routeros/pull/313). + - api_info, api_modify - add support for the ``routing igmp-proxy`` path (https://github.com/ansible-collections/community.routeros/pull/309). + - api_modify, api_info - add read-only ``default`` field to ``snmp community`` + (https://github.com/ansible-collections/community.routeros/pull/311). + release_summary: Feature release. + fragments: + - 2.19.0.yml + - 309-add-igmp-proxy.yml + - 310-add-ip-dns-adlist.yml + - 311-add-defaults-fields-snmp-community.yml + - 313-add-routing-filter-num-list.yml + - 315-bridge-mld-version-multicast-querier.yml + release_date: '2024-09-10' + 2.20.0: + changes: + minor_changes: + - api_info, api_modify - add new parameters from the RouterOS 7.16 release + (https://github.com/ansible-collections/community.routeros/pull/323). + - api_info, api_modify - add support ``interface l2tp-client`` configuration + (https://github.com/ansible-collections/community.routeros/pull/322). + - api_info, api_modify - add support for the ``cpu-frequency``, ``memory-frequency``, + ``preboot-etherboot`` and ``preboot-etherboot-server`` properties in ``system + routerboard settings`` (https://github.com/ansible-collections/community.routeros/pull/320). + - api_info, api_modify - add support for the ``matching-type`` property in + ``ip dhcp-server matcher`` introduced by RouterOS 7.16 (https://github.com/ansible-collections/community.routeros/pull/321). + release_summary: Feature release. + fragments: + - 2.20.0.yml + - 320-add-routerboard-properties.yml + - 321-dhcp-server-matcher-matching-type.yml + - 322-add-l2tp-client-interface-configuration.yml + - 323-add-ros-7.16-parameters.yml + release_date: '2024-10-17' + 3.0.0: + changes: + breaking_changes: + - command - the module no longer declares that it supports check mode (https://github.com/ansible-collections/community.routeros/pull/318). + release_summary: Major release that drops support for End of Life Python versions + and fixes check mode for community.routeros.command. + removed_features: + - The collection no longer supports Ansible 2.9, ansible-base 2.10, ansible-core + 2.11, ansible-core 2.12, ansible-core 2.13, and ansible-core 2.14. If you + need to continue using End of Life versions of Ansible/ansible-base/ansible-core, + please use community.routeros 2.x.y (https://github.com/ansible-collections/community.routeros/pull/318). + fragments: + - 3.0.0.yml + release_date: '2024-10-20' + 3.1.0: + changes: + bugfixes: + - api_info, api_modify - fields ``log`` and ``log-prefix`` in paths ``ip firewall + filter``, ``ip firewall mangle``, ``ip firewall nat``, ``ip firewall raw`` + now have the correct default values (https://github.com/ansible-collections/community.routeros/pull/324). + minor_changes: + - api_info, api_modify - add missing fields ``comment``, ``next-pool`` to + ``ip pool`` path (https://github.com/ansible-collections/community.routeros/pull/327). + release_summary: Bugfix and feature release. + fragments: + - 3.1.0.yml + - 324-fix-firewall-log-and-log-prefix.yaml + - 327-add-missing-ip-pool-fields.yml + release_date: '2024-12-02' + 3.2.0: + changes: + minor_changes: + - api_info, api_modify - add support for the ``routing filter community-list`` + path implemented by RouterOS 7 and newer (https://github.com/ansible-collections/community.routeros/pull/331). + release_summary: Feature release. + fragments: + - 3.2.0.yml + - 331-add-routing-filter-community-list.yml + release_date: '2024-12-30' + 3.3.0: + changes: + minor_changes: + - api_info, api_modify - add missing attribute ``require-message-auth`` for + the ``radius`` path which exists since RouterOS version 7.15 (https://github.com/ansible-collections/community.routeros/issues/338, + https://github.com/ansible-collections/community.routeros/pull/339). + - api_info, api_modify - add the ``interface 6to4`` path. Used to manage IPv6 + tunnels via tunnel-brokers like HE, where native IPv6 is not provided (https://github.com/ansible-collections/community.routeros/pull/342). + - api_info, api_modify - add the ``interface wireless access-list`` and ``interface + wireless connect-list`` paths (https://github.com/ansible-collections/community.routeros/issues/284, + https://github.com/ansible-collections/community.routeros/pull/340). + - api_info, api_modify - add the ``use-interface-duid`` option for ``ipv6 + dhcp-client`` path. This option prevents issues with Fritzbox modems and + routers, when using virtual interfaces (like VLANs) may create duplicated + records in hosts config, this breaks original "expose-host" function. Also + add the ``script``, ``custom-duid`` and ``validate-server-duid`` as backport + from 7.15 version update (https://github.com/ansible-collections/community.routeros/pull/341). + release_summary: Feature release. + fragments: + - 3.3.0.yml + - 339-add-require-message-auth-for-radius.yml + - 340-add-interface-wireless-access-and-connect-list.yml + - 341-add-dhcpv6-client-use-interface-duid.yml + - 342-add-interface-6to4.yml + release_date: '2025-01-27' + 3.4.0: + changes: + bugfixes: + - api_info, api_modify - remove the primary key ``action`` from the ``interface + wifi provisioning`` path, since RouterOS also allows to create completely + duplicate entries (https://github.com/ansible-collections/community.routeros/issues/344, + https://github.com/ansible-collections/community.routeros/pull/345). + minor_changes: + - api_info, api_modify - add support for the ``ip dns forwarders`` path implemented + by RouterOS 7.17 and newer (https://github.com/ansible-collections/community.routeros/pull/343). + release_summary: Feature and bugfix release. + fragments: + - 3.4.0.yml + - 343-add-ip-dns-forwarders.yml + - 345-interface-wifi-provisioning.yml + release_date: '2025-02-24' + 3.5.0: + changes: + minor_changes: + - api_info, api_modify - change default for ``/ip/cloud/ddns-enabled`` for + RouterOS 7.17 and newer from ``yes`` to ``auto`` (https://github.com/ansible-collections/community.routeros/pull/350). + release_summary: Feature release. + fragments: + - 3.5.0.yml + - 350-ip-cloud-ddns-enabled-auto.yml + release_date: '2025-03-22' + 3.6.0: + changes: + minor_changes: + - api_info, api_modify - add ``mdns-repeat-ifaces`` to ``ip dns`` for RouterOS + 7.16 and newer (https://github.com/ansible-collections/community.routeros/pull/358). + - api_info, api_modify - field name change in ``routing bgp connection`` path + implemented by RouterOS 7.19 and newer (https://github.com/ansible-collections/community.routeros/pull/360). + - api_info, api_modify - rename ``is-responder`` property in ``interface wireguard + peers`` to ``responder`` for RouterOS 7.17 and newer (https://github.com/ansible-collections/community.routeros/pull/364). + release_summary: Feature release. + fragments: + - 3.6.0.yml + - 358-mdns-repeat-ifaces.yml + - 360-bgp-connection-afi.yml + - 364-wireguard-responder.yml + release_date: '2025-04-21' + 3.7.0: + changes: + minor_changes: + - api_find_and_modify - allow to control whether ``dynamic`` and/or ``builtin`` + entries are ignored with the new ``ignore_dynamic`` and ``ignore_builtin`` + options (https://github.com/ansible-collections/community.routeros/issues/372, + https://github.com/ansible-collections/community.routeros/pull/373). + - api_info, api_modify - add ``port-cost-mode`` to ``interface bridge`` which + is supported since RouterOS 7.13 (https://github.com/ansible-collections/community.routeros/pull/371). + release_summary: Feature release. + fragments: + - 3.7.0.yml + - 371-add-bridge-port-cost-mode.yml + - 373-api_find_and_modify-dynamic-builtin.yml + release_date: '2025-05-31' + 3.8.0: + changes: + minor_changes: + - api_info, api_modify - add ``interface ethernet switch port-isolation`` + which is supported since RouterOS 6.43 (https://github.com/ansible-collections/community.routeros/pull/375). + - 'api_info, api_modify - add ``routing bfd configuration``. Officially stabilized + BFD support for BGP and OSPF is available since RouterOS 7.11 + + (https://github.com/ansible-collections/community.routeros/pull/375). + + ' + - api_modify, api_info - support API path ``ip ipsec mode-config`` (https://github.com/ansible-collections/community.routeros/pull/376). + release_summary: Feature release. + fragments: + - 3.8.0.yml + - 375-port_isolation-and-routing_bfd_configuration.yml + - 376-ipsec-mode-config.yml + release_date: '2025-06-14' + 3.8.1: + changes: + bugfixes: + - facts and api_facts modules - prevent deprecation warnings when used with + ansible-core 2.19 (https://github.com/ansible-collections/community.routeros/pull/384). + release_summary: Bugfix release. + fragments: + - 3.8.1.yml + - 384-warnings.yml + release_date: '2025-07-26' + 3.9.0: + changes: + bugfixes: + - routeros terminal plugin - fix ``terminal_stdout_re`` pattern to handle + long system identities when connecting to RouterOS through SSH (https://github.com/ansible-collections/community.routeros/pull/386). + minor_changes: + - api_info, api modify - add ``remote-log-format``, ``remote-protocol``, and + ``event-delimiter`` to ``system logging action`` (https://github.com/ansible-collections/community.routeros/pull/381). + - api_info, api_modify - add ``disable-link-local-address`` and ``stale-neighbor-timeout`` + fields to ``ipv6 settings`` (https://github.com/ansible-collections/community.routeros/pull/380). + - api_info, api_modify - adjust neighbor limit fields in ``ipv6 settings`` + to match RouterOS 7.18 and newer (https://github.com/ansible-collections/community.routeros/pull/380). + - api_info, api_modify - set ``passthrough`` default in ``ip firewall mangle`` + to ``true`` for RouterOS 7.19 and newer (https://github.com/ansible-collections/community.routeros/pull/382). + - api_info, api_modify - since RouterOS 7.17 VRF is supported for OVPN server. + It now supports multiple entries, while ``api_modify`` so far only accepted + a single entry. The ``interface ovpn-server server`` path now allows multiple + entries on RouterOS 7.17 and newer (https://github.com/ansible-collections/community.routeros/pull/383). + release_summary: Bugfix and feature release. + fragments: + - 3.9.0.yml + - 380-ipv6-settings.yml + - 381-logging-cef.yml + - 382-mangle-passthrough.yml + - 385-vrf-support-for-ovpn-server.yml + - 386-fix-pattern-to-handle-long-identity.yml + release_date: '2025-08-10' diff --git a/changelogs/changelog.yaml.license b/changelogs/changelog.yaml.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/changelogs/changelog.yaml.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/changelogs/config.yaml b/changelogs/config.yaml index 8f014ed..39b7120 100644 --- a/changelogs/config.yaml +++ b/changelogs/config.yaml @@ -1,29 +1,43 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + changelog_filename_template: ../CHANGELOG.rst changelog_filename_version_depth: 0 changes_file: changelog.yaml changes_format: combined +ignore_other_fragment_extensions: true keep_fragments: false mention_ancestor: true -flatmap: true new_plugins_after_name: removed_features notesdir: fragments +output_formats: + - rst + - md prelude_section_name: release_summary prelude_section_title: Release Summary sections: -- - major_changes - - Major Changes -- - minor_changes - - Minor Changes -- - breaking_changes - - Breaking Changes / Porting Guide -- - deprecated_features - - Deprecated Features -- - removed_features - - Removed Features (previously deprecated) -- - security_fixes - - Security Fixes -- - bugfixes - - Bugfixes -- - known_issues - - Known Issues + - - major_changes + - Major Changes + - - minor_changes + - Minor Changes + - - breaking_changes + - Breaking Changes / Porting Guide + - - deprecated_features + - Deprecated Features + - - removed_features + - Removed Features (previously deprecated) + - - security_fixes + - Security Fixes + - - bugfixes + - Bugfixes + - - known_issues + - Known Issues title: Community RouterOS +trivial_section_name: trivial +use_fqcn: true +add_plugin_period: true +changelog_nice_yaml: true +changelog_sort: version +vcs: auto diff --git a/changelogs/fragments/391-report-unknown-interfaces.yml b/changelogs/fragments/391-report-unknown-interfaces.yml new file mode 100644 index 0000000..bcef417 --- /dev/null +++ b/changelogs/fragments/391-report-unknown-interfaces.yml @@ -0,0 +1,12 @@ +--- +bugfixes: + - | + api_facts - also report interfaces that are inferred only by reference by IP addresses. + RouterOS's APIs have IPv4 and IPv6 addresses point at interfaces by their name, which can + change over time and in-between API calls, such that interfaces may have been enumerated + under another name, or not at all (for example when removed). Such interfaces are now reported + under their new or temporary name and with a synthetic ``type`` property set to differentiate + the more likely and positively confirmed removal case (with ``type: "ansible:unknown"``) from + the unlikely and probably transient naming mismatch (with ``type: "ansible:mismatch"``). + Previously, the api_facts module would have crashed with a ``KeyError`` exception + (https://github.com/ansible-collections/community.routeros/pull/391). diff --git a/changelogs/fragments/392-sys-note-cli-login.yml b/changelogs/fragments/392-sys-note-cli-login.yml new file mode 100644 index 0000000..618db86 --- /dev/null +++ b/changelogs/fragments/392-sys-note-cli-login.yml @@ -0,0 +1,2 @@ +minor_changes: + - api_info, api_modify - add ``show-at-cli-login`` property in ``system note`` (https://github.com/ansible-collections/community.routeros/pull/392). diff --git a/changelogs/fragments/394-iface-list-defaults.yml b/changelogs/fragments/394-iface-list-defaults.yml new file mode 100644 index 0000000..7e517a6 --- /dev/null +++ b/changelogs/fragments/394-iface-list-defaults.yml @@ -0,0 +1,2 @@ +minor_changes: + - api_info, api_modify - set default value for ``include`` and ``exclude`` properties in ``system note`` to an empty string (https://github.com/ansible-collections/community.routeros/pull/394). diff --git a/codecov.yml b/codecov.yml index 724e063..3b2f9ed 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + fixes: - "ansible_collections/community/routeros/::" diff --git a/docs/docsite/config.yml b/docs/docsite/config.yml new file mode 100644 index 0000000..1d6cf85 --- /dev/null +++ b/docs/docsite/config.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +changelog: + write_changelog: true diff --git a/docs/docsite/extra-docs.yml b/docs/docsite/extra-docs.yml index cb2ef6a..6091591 100644 --- a/docs/docsite/extra-docs.yml +++ b/docs/docsite/extra-docs.yml @@ -1,6 +1,11 @@ --- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + sections: - title: Guides toctree: - api-guide - ssh-guide + - quoting diff --git a/docs/docsite/links.yml b/docs/docsite/links.yml new file mode 100644 index 0000000..0c68c84 --- /dev/null +++ b/docs/docsite/links.yml @@ -0,0 +1,33 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +edit_on_github: + repository: ansible-collections/community.routeros + branch: main + path_prefix: '' + +extra_links: + - description: Ask for help (RouterOS) + url: https://forum.ansible.com/tags/c/help/6/none/routeros + - description: Submit a bug report + url: https://github.com/ansible-collections/community.routeros/issues/new?assignees=&labels=&template=bug_report.md + - description: Request a feature + url: https://github.com/ansible-collections/community.routeros/issues/new?assignees=&labels=&template=feature_request.md + +communication: + matrix_rooms: + - topic: General usage and support questions + room: '#users:ansible.im' + irc_channels: + - topic: General usage and support questions + network: Libera + channel: '#ansible' + forums: + - topic: "Ansible Forum: General usage and support questions" + # The following URL directly points to the "Get Help" section + url: https://forum.ansible.com/c/help/6/none + - topic: "Ansible Forum: Discussions about RouterOS" + # The following URL directly points to the "routeros" tag + url: https://forum.ansible.com/tag/routeros diff --git a/docs/docsite/rst/api-guide.rst b/docs/docsite/rst/api-guide.rst index 482af9a..9df17fc 100644 --- a/docs/docsite/rst/api-guide.rst +++ b/docs/docsite/rst/api-guide.rst @@ -1,9 +1,14 @@ +.. + Copyright (c) Ansible Project + GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) + SPDX-License-Identifier: GPL-3.0-or-later + .. _ansible_collections.community.routeros.docsite.api-guide: How to connect to RouterOS devices with the RouterOS API ======================================================== -You can use the :ref:`community.routeros.api module ` to connect to a RouterOS device with the RouterOS API. +You can use the :ansplugin:`community.routeros.api module ` to connect to a RouterOS device with the RouterOS API. More specific module to modify certain entries are the :ansplugin:`community.routeros.api_modify ` and :ansplugin:`community.routeros.api_find_and_modify ` modules. The :ansplugin:`community.routeros.api_info module ` allows to retrieve information on specific predefined paths that can be used as input for the :ansplugin:`community.routeros.api_modify ` module, and the :ansplugin:`community.routeros.api_facts module ` allows to retrieve Ansible facts using the RouterOS API. No special setup is needed; the module needs to be run on a host that can connect to the device's API. The most common case is that the module is run on ``localhost``, either by using ``hosts: localhost`` in the playbook, or by using ``delegate_to: localhost`` for the task. The following example shows how to run the equivalent of ``/ip address print``: @@ -12,7 +17,7 @@ No special setup is needed; the module needs to be run on a host that can connec --- - name: RouterOS test with API hosts: localhost - gather_facts: no + gather_facts: false vars: hostname: 192.168.1.1 username: admin @@ -33,9 +38,9 @@ No special setup is needed; the module needs to be run on a host that can connec # ca_path: /path/to/ca-certificate.pem register: print_path - - name: Show IP address of first interface - debug: - msg: "{{ print_path.msg[0].address }}" + - name: Show IP address of first interface + ansible.builtin.debug: + msg: "{{ print_path.msg[0].address }}" This results in the following output: @@ -52,18 +57,60 @@ This results in the following output: } PLAY RECAP ******************************************************************************************************* - localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 -Check out the documenation of the :ref:`community.routeros.api module ` for details on the options. +Check out the documentation of the :ansplugin:`community.routeros.api module ` for details on the options. + +Using the ``community.routeros.api`` module defaults group +---------------------------------------------------------- + +To avoid having to specify common parameters for all the API based modules in every task, you can use the ``community.routeros.api`` :ref:`module defaults group `: + +.. code-block:: yaml+jinja + + --- + - name: RouterOS test with API + hosts: localhost + gather_facts: false + module_defaults: + group/community.routeros.api: + hostname: 192.168.1.1 + password: admin + username: test1234 + # The following options configure TLS/SSL. + # Depending on your setup, these options need different values: + tls: true + validate_certs: true + validate_cert_hostname: true + # If you are using your own PKI, specify the path to your CA certificate here: + # ca_path: /path/to/ca-certificate.pem + tasks: + - name: Gather facts + community.routeros.api_facts: + + - name: Get "ip address print" + community.routeros.api: + path: "ip address" + + - name: Change IP address to 192.168.1.1 for interface bridge + community.routeros.api_find_and_modify: + path: ip address + find: + interface: bridge + values: + address: "192.168.1.1/24" + +Here all three tasks will use the options set for the module defaults group. Setting up encryption --------------------- -It is recommended to always use ``tls: true`` when connecting with the API, even if you are only connecting to the device through a trusted network. The following options control how TLS/SSL is used: +It is recommended to always use :ansopt:`tls=true` when connecting with the API, even if you are only connecting to the device through a trusted network. The following options control how TLS/SSL is used: -:validate_certs: Setting to ``false`` disables any certificate validation. **This is discouraged to use in production**, but is needed when setting the device up. The default value is ``true``. -:validate_cert_hostname: Setting to ``false`` (default) disables hostname verification during certificate validation. This is needed if the hostnames specified in the certificate do not match the hostname used for connecting (usually the device's IP). It is recommended to set up the certificate correctly and set this to ``true``; the default ``false`` is chosen for backwards compatibility to an older version of the module. -:ca_path: If you are not using a commerically trusted CA certificate to sign your device's certificate, or have not included your CA certificate in Python's truststore, you need to point this option to the CA certificate. +:force_no_cert: Setting to :ansval:`true` connects to the device without a certificate. **This is discouraged to use in production and is susceptible to Man-in-the-Middle attacks**, but might be useful when setting the device up. The default value is :ansval:`false`. +:validate_certs: Setting to :ansval:`false` disables any certificate validation. **This is discouraged to use in production**, but is needed when setting the device up. The default value is :ansval:`true`. +:validate_cert_hostname: Setting to :ansval:`false` (default) disables hostname verification during certificate validation. This is needed if the hostnames specified in the certificate do not match the hostname used for connecting (usually the device's IP). It is recommended to set up the certificate correctly and set this to :ansval:`true`; the default :ansval:`false` is chosen for backwards compatibility to an older version of the module. +:ca_path: If you are not using a commercially trusted CA certificate to sign your device's certificate, or have not included your CA certificate in Python's truststore, you need to point this option to the CA certificate. We recommend to create a CA certificate that is used to sign the certificates for your RouterOS devices, and have the certificates include the correct hostname(s), including the IP of the device. That way, you can fully enable TLS and be sure that you always talk to the correct device. @@ -77,7 +124,7 @@ Installing a certificate on a MikroTik router Installing the certificate is best done with the SSH connection. (See the :ref:`ansible_collections.community.routeros.docsite.ssh-guide` guide for more information.) Once the certificate has been installed, and the HTTPS API enabled, it's easier to work with the API, since it has a quite a few less problems, and returns data as JSON objects instead of text you first have to parse. -First you have to convert the certificate and its private key to a `PKCS #12 bundle `_. This can be done with the :ref:`community.crypto.openssl_pkcs12 `. The following playbook assumes that the certificate is available as ``keys/{{ inventory_hostname }}.pem``, and its private key is available as ``keys/{{ inventory_hostname }}.key``. It generates a random passphrase to protect the PKCS#12 file. +First you have to convert the certificate and its private key to a `PKCS #12 bundle `_. This can be done with the :ansplugin:`community.crypto.openssl_pkcs12 `. The following playbook assumes that the certificate is available as ``keys/{{ inventory_hostname }}.pem``, and its private key is available as ``keys/{{ inventory_hostname }}.key``. It generates a random passphrase to protect the PKCS#12 file. .. code-block:: yaml+jinja @@ -139,12 +186,12 @@ First you have to convert the certificate and its private key to a `PKCS #12 bun The playbook also assumes that ``admin_network`` describes the network from which the HTTPS and API interface can be accessed. This can be for example ``192.168.1.0/24``. -When this playbook completed successfully, you should be able to use the HTTPS admin interface (reachable in a browser from ``https://192.168.1.1/``, with the correct IP inserted), as well as the :ref:`community.routeros.api module ` module with TLS and certificate validation enabled: +When this playbook completed successfully, you should be able to use the HTTPS admin interface (reachable in a browser from ``https://192.168.1.1/``, with the correct IP inserted), as well as the :ansplugin:`community.routeros.api module ` module with TLS and certificate validation enabled: .. code-block:: yaml+jinja - community.routeros.api: - ... + # ... tls: true validate_certs: true validate_cert_hostname: true diff --git a/docs/docsite/rst/quoting.rst b/docs/docsite/rst/quoting.rst new file mode 100644 index 0000000..b1c0453 --- /dev/null +++ b/docs/docsite/rst/quoting.rst @@ -0,0 +1,19 @@ +.. + Copyright (c) Ansible Project + GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) + SPDX-License-Identifier: GPL-3.0-or-later + +.. _ansible_collections.community.routeros.docsite.quoting: + +How to quote and unquote commands and arguments +=============================================== + +When using the :ansplugin:`community.routeros.command module ` or the :ansplugin:`community.routeros.api module ` modules, you need to pass text data in quoted form. While in some cases quoting is not needed (when passing IP addresses or names without spaces, for example), in other cases it is required, like when passing a comment which contains a space. + +The community.routeros collection provides a set of Jinja2 filter plugins which helps you with these tasks: + +- The :ansplugin:`community.routeros.quote_argument_value filter ` quotes an argument value: ``'this is a "comment"' | community.routeros.quote_argument_value == '"this is a \\"comment\\""'``. +- The :ansplugin:`community.routeros.quote_argument filter ` quotes an argument with or without a value: ``'comment=this is a "comment"' | community.routeros.quote_argument == 'comment="this is a \\"comment\\""'``. +- The :ansplugin:`community.routeros.join filter ` quotes a list of arguments and joins them to one string: ``['foo=bar', 'comment=foo is bar'] | community.routeros.join == 'foo=bar comment="foo is bar"'``. +- The :ansplugin:`community.routeros.split filter ` splits a command into a list of arguments (with or without values): ``'foo=bar comment="foo is bar"' | community.routeros.split == ['foo=bar', 'comment=foo is bar']`` +- The :ansplugin:`community.routeros.list_to_dict filter ` splits a list of arguments with values into a dictionary: ``['foo=bar', 'comment=foo is bar'] | community.routeros.list_to_dict == {'foo': 'bar', 'comment': 'foo is bar'}``. It has two optional arguments: :ansopt:`community.routeros.list_to_dict#filter:require_assignment` (default value :ansval:`true`) allows to accept arguments without values when set to :ansval:`false`; and :ansopt:`community.routeros.list_to_dict#filter:skip_empty_values` (default value :ansval:`false`) allows to skip arguments whose value is empty. diff --git a/docs/docsite/rst/ssh-guide.rst b/docs/docsite/rst/ssh-guide.rst index 4c59d5c..ac1f65b 100644 --- a/docs/docsite/rst/ssh-guide.rst +++ b/docs/docsite/rst/ssh-guide.rst @@ -1,3 +1,8 @@ +.. + Copyright (c) Ansible Project + GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) + SPDX-License-Identifier: GPL-3.0-or-later + .. _ansible_collections.community.routeros.docsite.ssh-guide: How to connect to RouterOS devices with SSH @@ -5,17 +10,17 @@ How to connect to RouterOS devices with SSH The collection offers two modules to connect to RouterOS devies with SSH: -- The :ref:`community.routeros.facts module ` gathers facts about a RouterOS device; -- The :ref:`community.routeros.command module ` executes commands on a RouterOS device. +- The :ansplugin:`community.routeros.facts module ` gathers facts about a RouterOS device; +- The :ansplugin:`community.routeros.command module ` executes commands on a RouterOS device. -The modules need the :ref:`ansible.netcommon.network_cli connection plugin ` for this. +The modules need the :ansplugin:`ansible.netcommon.network_cli connection plugin ` for this. Important notes --------------- 1. The SSH-based modules do not support arbitrary symbols in the router's identity. If you are having trouble connecting to your device, please make sure that your MikroTik's identity contains only alphanumeric characters and dashes. Also make sure that the identity string is not longer than 19 characters (`see issue for details `__). Similar problems can happen for unsupported characters in your username. -2. The :ref:`community.routeros.command module ` does not support nesting commands and expects every command to start with a forward slash (``/``). Running the following command will produce an error: +2. The :ansplugin:`community.routeros.command module ` does not support nesting commands and expects every command to start with a forward slash (``/``). Running the following command will produce an error: .. code-block:: yaml+jinja @@ -24,9 +29,11 @@ Important notes - /ip - print -3. When using the :ref:`community.routeros.command module ` module, make sure to not specify too long commands. Alternatively, add something like ``+cet512w`` to the username (replace ``admin`` with ``admin+cet512w``) to tell RouterOS to not wrap before 512 characters in a line (`see issue for details `__). +3. When using the :ansplugin:`community.routeros.command module ` module, make sure to not specify too long commands. Alternatively, add something like ``+cet512w`` to the username (replace ``admin`` with ``admin+cet512w``) to tell RouterOS to not wrap before 512 characters in a line (`see issue for details `__). -4. Finally, the :ref:`ansible.netcommon.network_cli connection plugin ` uses `paramiko `_ by default to connect to devices with SSH. You can set its ``ssh_type`` option to ``libssh`` to use `ansible-pylibssh `_ instead, which offers Python bindings to libssh. See its documentation for details. +4. The :ansplugin:`ansible.netcommon.network_cli connection plugin ` uses `paramiko `_ by default to connect to devices with SSH. You can set its :ansopt:`ansible.netcommon.network_cli#connection:ssh_type` option to :ansval:`libssh` to use `ansible-pylibssh `_ instead, which offers Python bindings to libssh. See its documentation for details. + +5. User is **not allowed** to login via SSH by password to modern Mikrotik if SSH key for the user is added! Setting up an inventory ----------------------- @@ -44,7 +51,7 @@ An example inventory ``hosts`` file for a RouterOS device is as follows: ansible_user=admin ansible_ssh_pass=test1234 -This tells Ansible that you have a RouterOS device called ``router`` with IP ``192.168.2.1``. Ansible should use the :ref:`ansible.netcommon.network_cli connection plugin ` together with the the :ref:`community.routeros.routeros cliconf plugin `. The credentials are stored as ``ansible_user`` and ``ansible_ssh_pass`` in the inventory. +This tells Ansible that you have a RouterOS device called ``router`` with IP ``192.168.2.1``. Ansible should use the :ansplugin:`ansible.netcommon.network_cli connection plugin ` together with the the :ansplugin:`community.routeros.routeros cliconf plugin `. The credentials are stored as ``ansible_user`` and ``ansible_ssh_pass`` in the inventory. Connecting to the device ------------------------ @@ -59,22 +66,22 @@ With the above inventory, you can use the following playbook to execute ``/syste gather_facts: false tasks: - - name: Gather system resources - community.routeros.command: - commands: - - /system resource print - register: system_resource_print + - name: Gather system resources + community.routeros.command: + commands: + - /system resource print + register: system_resource_print - - name: Show system resources - debug: - var: system_resource_print.stdout_lines + - name: Show system resources + debug: + var: system_resource_print.stdout_lines - - name: Gather facts - community.routeros.facts: + - name: Gather facts + community.routeros.facts: - - name: Show a fact - debug: - msg: "First IP address: {{ ansible_net_all_ipv4_addresses[0] }}" + - name: Show a fact + debug: + msg: "First IP address: {{ ansible_net_all_ipv4_addresses[0] }}" This results in the following output: @@ -119,4 +126,4 @@ This results in the following output: } PLAY RECAP ******************************************************************************************************* - router : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + router : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 diff --git a/galaxy.yml b/galaxy.yml index f83ca29..f2ba31a 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,14 +1,22 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + # See https://docs.ansible.com/ansible/latest/dev_guide/collections_galaxy_meta.html namespace: community name: routeros -version: 2.0.0-a1 +version: 3.9.0 readme: README.md authors: - Egor Zaitsev (github.com/heuels) - Nikolay Dachev (github.com/NikolayDachev) -description: Modules for MikroTik RouterOS -license_file: COPYING + - Felix Fontein (github.com/felixfontein) +description: Modules and plugins for MikroTik RouterOS +license: + - GPL-3.0-or-later +# license_file: COPYING tags: - network - mikrotik diff --git a/meta/ee-requirements.txt b/meta/ee-requirements.txt new file mode 100644 index 0000000..a36140c --- /dev/null +++ b/meta/ee-requirements.txt @@ -0,0 +1,5 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +librouteros diff --git a/meta/execution-environment.yml b/meta/execution-environment.yml new file mode 100644 index 0000000..ac7ebac --- /dev/null +++ b/meta/execution-environment.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +version: 1 +dependencies: + python: meta/ee-requirements.txt diff --git a/meta/runtime.yml b/meta/runtime.yml index 2ee3c9f..aae1259 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,2 +1,13 @@ --- -requires_ansible: '>=2.9.10' +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +requires_ansible: '>=2.15.0' +action_groups: + api: + - api + - api_facts + - api_find_and_modify + - api_info + - api_modify diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..16187f9 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,53 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# The following metadata allows Python runners and nox to install the required +# dependencies for running this Python script: +# +# /// script +# dependencies = ["nox>=2025.02.09", "antsibull-nox"] +# /// + +import os +import sys + +import nox + + +# We try to import antsibull-nox, and if that doesn't work, provide a more useful +# error message to the user. +try: + import antsibull_nox +except ImportError: + print("You need to install antsibull-nox in the same Python environment as nox.") + sys.exit(1) + + +IN_CI = os.environ.get("CI") == "true" + + +antsibull_nox.load_antsibull_nox_toml() + + +@nox.session(name="update-docs", default=True) +def update_docs_fragments(session: nox.Session) -> None: + """ + Update/check auto-generated parts of docs fragments. + """ + session.install("ansible-core") + prepare = antsibull_nox.sessions.prepare_collections( + session, install_in_site_packages=True + ) + if not prepare: + return + data = ["python", "tests/update-docs.py"] + if IN_CI: + data.append("--lint") + session.run(*data) + + +# Allow to run the noxfile with `python noxfile.py`, `pipx run noxfile.py`, or similar. +# Requires nox >= 2025.02.09 +if __name__ == "__main__": + nox.main() diff --git a/plugins/cliconf/routeros.py b/plugins/cliconf/routeros.py index 6c49373..dc2f4a9 100644 --- a/plugins/cliconf/routeros.py +++ b/plugins/cliconf/routeros.py @@ -1,42 +1,24 @@ -# -# (c) 2017 Red Hat Inc. -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . -# +# Copyright (c) 2017 Red Hat Inc. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import (absolute_import, division, print_function) __metaclass__ = type -DOCUMENTATION = ''' ---- +DOCUMENTATION = r""" author: "Egor Zaitsev (@heuels)" name: routeros short_description: Use routeros cliconf to run command on MikroTik RouterOS platform description: - - This routeros plugin provides low level abstraction apis for - sending and receiving CLI commands from MikroTik RouterOS network devices. -''' + - This routeros plugin provides low level abstraction APIs for sending and receiving CLI commands from MikroTik RouterOS + network devices. +""" import re import json -from itertools import chain - -from ansible.module_utils.common.text.converters import to_bytes, to_text -from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list -from ansible.plugins.cliconf import CliconfBase, enable_mode +from ansible.module_utils.common.text.converters import to_text +from ansible.plugins.cliconf import CliconfBase class Cliconf(CliconfBase): @@ -65,7 +47,7 @@ class Cliconf(CliconfBase): return device_info - def get_config(self, source='running', format='text', flags=None): + def get_config(self, source='running', flags=None, format=None): return def edit_config(self, command): diff --git a/plugins/doc_fragments/api.py b/plugins/doc_fragments/api.py new file mode 100644 index 0000000..5b0b411 --- /dev/null +++ b/plugins/doc_fragments/api.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Nikolay Dachev +# GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + DOCUMENTATION = r""" +options: + hostname: + description: + - RouterOS hostname API. + required: true + type: str + username: + description: + - RouterOS login user. + required: true + type: str + password: + description: + - RouterOS user password. + required: true + type: str + timeout: + description: + - Timeout for the request. + type: int + default: 10 + version_added: 2.3.0 + tls: + description: + - If is set TLS will be used for RouterOS API connection. + required: false + type: bool + default: false + aliases: + - ssl + port: + description: + - RouterOS API port. If O(tls) is set, port will apply to TLS/SSL connection. + - Defaults are V(8728) for the HTTP API, and V(8729) for the HTTPS API. + type: int + force_no_cert: + description: + - Set to V(true) to connect without a certificate when O(tls=true). + - See also O(validate_certs). + - B(Note:) this forces the use of anonymous Diffie-Hellman (ADH) ciphers. The protocol is susceptible to Man-in-the-Middle + attacks, because the keys used in the exchange are not authenticated. Instead of simply connecting without a certificate + to "make things work" have a look at O(validate_certs) and O(ca_path). + type: bool + default: false + version_added: 2.4.0 + validate_certs: + description: + - Set to V(false) to skip validation of TLS certificates. + - See also O(validate_cert_hostname). Only used when O(tls=true). + - B(Note:) instead of simply deactivating certificate validations to "make things work", please consider creating your + own CA certificate and using it to sign certificates used for your router. You can tell the module about your CA certificate + with the O(ca_path) option. + type: bool + default: true + version_added: 1.2.0 + validate_cert_hostname: + description: + - Set to V(true) to validate hostnames in certificates. + - See also O(validate_certs). Only used when O(tls=true) and O(validate_certs=true). + type: bool + default: false + version_added: 1.2.0 + ca_path: + description: + - PEM formatted file that contains a CA certificate to be used for certificate validation. + - See also O(validate_cert_hostname). Only used when O(tls=true) and O(validate_certs=true). + type: path + version_added: 1.2.0 + encoding: + description: + - Use the specified encoding when communicating with the RouterOS device. + - Default is V(ASCII). Note that V(UTF-8) requires librouteros 3.2.1 or newer. + type: str + default: ASCII + version_added: 2.1.0 +requirements: + - librouteros + - Python >= 3.6 (for librouteros) +seealso: + - ref: ansible_collections.community.routeros.docsite.api-guide + description: How to connect to RouterOS devices with the RouterOS API. +""" + + RESTRICT = r""" +options: + restrict: + type: list + elements: dict + suboptions: + field: + description: + - The field whose values to restrict. + required: true + type: str + match_disabled: + description: + - Whether disabled or not provided values should match. + type: bool + default: false + values: + description: + - The values of the field to limit to. + - 'Note that the types of the values are important. If you provide a string V("0"), and librouteros converts the + value returned by the API to the integer V(0), then this will not match. If you are not sure, better include both + variants: both the string and the integer.' + type: list + elements: raw + regex: + description: + - A regular expression matching values of the field to limit to. + - Note that all values will be converted to strings before matching. + - It is not possible to match disabled values with regular expressions. Set O(restrict[].match_disabled=true) if + you also want to match disabled values. + type: str + invert: + description: + - Invert the condition. This affects O(restrict[].match_disabled), O(restrict[].values), and O(restrict[].regex). + type: bool + default: false +""" diff --git a/plugins/doc_fragments/attributes.py b/plugins/doc_fragments/attributes.py new file mode 100644 index 0000000..b8e68bf --- /dev/null +++ b/plugins/doc_fragments/attributes.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard documentation fragment + DOCUMENTATION = r""" +options: {} +attributes: + check_mode: + description: Can run in C(check_mode) and return changed status prediction without modifying target. + diff_mode: + description: Will return details on what has changed (or possibly needs changing in C(check_mode)), when in diff mode. + platform: + description: Target OS/families that can be operated against. + support: N/A + idempotent: + description: + - When run twice in a row outside check mode, with the same arguments, the second invocation indicates no change. + - This assumes that the system controlled/queried by the module has not changed in a relevant way. +""" + + # Should be used together with the standard fragment + IDEMPOTENT_NOT_MODIFY_STATE = r""" +options: {} +attributes: + idempotent: + support: full + details: + - This action does not modify state. +""" + + # Should be used together with the standard fragment + INFO_MODULE = r''' +options: {} +attributes: + check_mode: + support: full + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. +''' + + ACTIONGROUP_API = r''' +options: {} +attributes: + action_group: + description: Use C(group/community.routeros.api) in C(module_defaults) to set defaults for this module. + support: full + membership: + - community.routeros.api +''' + + CONN = r""" +options: {} +attributes: + become: + description: Is usable alongside C(become) keywords. + connection: + description: Uses the target's configured connection information to execute code on it. + delegation: + description: Can be used in conjunction with C(delegate_to) and related keywords. +""" + + FACTS = r""" +options: {} +attributes: + facts: + description: Action returns an C(ansible_facts) dictionary that will update existing host facts. +""" + + # Should be used together with the standard fragment and the FACTS fragment + FACTS_MODULE = r''' +options: {} +attributes: + check_mode: + support: full + details: + - This action does not modify state. + diff_mode: + support: N/A + details: + - This action does not modify state. + facts: + support: full +''' + + FILES = r""" +options: {} +attributes: + safe_file_operations: + description: Uses Ansible's strict file operation functions to ensure proper permissions and avoid data corruption. +""" + + FLOW = r""" +options: {} +attributes: + action: + description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller. + async: + description: Supports being used with the C(async) keyword. +""" diff --git a/plugins/filter/join.yml b/plugins/filter/join.yml new file mode 100644 index 0000000..f25d739 --- /dev/null +++ b/plugins/filter/join.yml @@ -0,0 +1,32 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +DOCUMENTATION: + name: join + short_description: Join a list of arguments to a command + version_added: 2.0.0 + description: + - Join and quotes a list of arguments to a command. + options: + _input: + description: + - A list of arguments to quote and join. + type: list + elements: string + required: true + author: + - Felix Fontein (@felixfontein) + +EXAMPLES: | + --- + - name: Join arguments for a RouterOS CLI command + ansible.builtin.set_fact: + arguments: "{{ ['foo=bar', 'comment=foo is bar'] | community.routeros.join }}" + # Should result in 'foo=bar comment="foo is bar"' + +RETURN: + _value: + description: The joined and quoted result. + type: string diff --git a/plugins/filter/list_to_dict.yml b/plugins/filter/list_to_dict.yml new file mode 100644 index 0000000..7b7c5b1 --- /dev/null +++ b/plugins/filter/list_to_dict.yml @@ -0,0 +1,42 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +DOCUMENTATION: + name: list_to_dict + short_description: Convert a list of arguments to a dictionary + version_added: 2.0.0 + description: + - Convert a list of arguments to a dictionary. + options: + _input: + description: + - A list of assignments. Can be the result of the P(community.routeros.split#filter) filter. + type: list + elements: string + required: true + require_assignment: + description: + - Allows to accept arguments without values when set to V(false). + type: boolean + default: true + skip_empty_values: + description: + - Allows to skip arguments whose value is empty when set to V(true). + type: boolean + default: false + author: + - Felix Fontein (@felixfontein) + +EXAMPLES: | + --- + - name: Convert a list to a dictionary + ansible.builtin.set_fact: + dictionary: "{{ ['foo=bar', 'comment=foo is bar'] | community.routeros.list_to_dict }}" + # dictionary == {'foo': 'bar', 'comment': 'foo is bar'} + +RETURN: + _value: + description: A dictionary representation of the input data. + type: dictionary diff --git a/plugins/filter/quote_argument.yml b/plugins/filter/quote_argument.yml new file mode 100644 index 0000000..477a15e --- /dev/null +++ b/plugins/filter/quote_argument.yml @@ -0,0 +1,32 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +DOCUMENTATION: + name: quote_argument + short_description: Quote an argument + version_added: 2.0.0 + description: + - Quote an argument. + options: + _input: + description: + - An argument to quote. + type: string + required: true + author: + - Felix Fontein (@felixfontein) + +EXAMPLES: | + --- + - name: Quote a RouterOS CLI command argument + ansible.builtin.set_fact: + quoted: >- + {{ 'comment=this is a "comment"' | community.routeros.quote_argument }} + # Should result in 'comment="this is a \"comment\""' + +RETURN: + _value: + description: The quoted argument. + type: string diff --git a/plugins/filter/quote_argument_value.yml b/plugins/filter/quote_argument_value.yml new file mode 100644 index 0000000..b4da246 --- /dev/null +++ b/plugins/filter/quote_argument_value.yml @@ -0,0 +1,32 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +DOCUMENTATION: + name: quote_argument_value + short_description: Quote an argument value + version_added: 2.0.0 + description: + - Quote an argument value. + options: + _input: + description: + - An argument value to quote. + type: string + required: true + author: + - Felix Fontein (@felixfontein) + +EXAMPLES: | + --- + - name: Quote a RouterOS CLI command argument's value + ansible.builtin.set_fact: + quoted: >- + {{ 'this is a "comment"' | community.routeros.quote_argument_value }} + # Should result in '"this is a \"comment\""' + +RETURN: + _value: + description: The quoted argument value. + type: string diff --git a/plugins/filter/quoting.py b/plugins/filter/quoting.py new file mode 100644 index 0000000..3985d55 --- /dev/null +++ b/plugins/filter/quoting.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.common.text.converters import to_text + +from ansible_collections.community.routeros.plugins.module_utils.quoting import ( + ParseError, + convert_list_to_dictionary, + join_routeros_command, + quote_routeros_argument, + quote_routeros_argument_value, + split_routeros_command, +) + + +def wrap_exception(fn, *args, **kwargs): + try: + return fn(*args, **kwargs) + except ParseError as e: + raise AnsibleFilterError(to_text(e)) + + +def split(line): + ''' + Split a command into arguments. + + Example: + 'add name=wrap comment="with space"' + is converted to: + ['add', 'name=wrap', 'comment=with space'] + ''' + return wrap_exception(split_routeros_command, line) + + +def quote_argument_value(argument): + ''' + Quote an argument value. + + Example: + 'with "space"' + is converted to: + r'"with \"space\""' + ''' + return wrap_exception(quote_routeros_argument_value, argument) + + +def quote_argument(argument): + ''' + Quote an argument. + + Example: + 'comment=with "space"' + is converted to: + r'comment="with \"space\""' + ''' + return wrap_exception(quote_routeros_argument, argument) + + +def join(arguments): + ''' + Join a list of arguments to a command. + + Example: + ['add', 'name=wrap', 'comment=with space'] + is converted to: + 'add name=wrap comment="with space"' + ''' + return wrap_exception(join_routeros_command, arguments) + + +def list_to_dict(string_list, require_assignment=True, skip_empty_values=False): + ''' + Convert a list of arguments to a list of dictionary. + + Example: + ['foo=bar', 'comment=with space', 'additional='] + is converted to: + {'foo': 'bar', 'comment': 'with space', 'additional': ''} + + If require_assignment is True (default), arguments without assignments are + rejected. (Example: in ['add', 'name=foo'], 'add' is an argument without + assignment.) If it is False, these are given value None. + + If skip_empty_values is True, arguments with empty value are removed from + the result. (Example: in ['name='], 'name' has an empty value.) + If it is False (default), these are kept. + + ''' + return wrap_exception( + convert_list_to_dictionary, + string_list, + require_assignment=require_assignment, + skip_empty_values=skip_empty_values, + ) + + +class FilterModule(object): + '''Ansible jinja2 filters for RouterOS command quoting and unquoting''' + + def filters(self): + return { + 'split': split, + 'quote_argument': quote_argument, + 'quote_argument_value': quote_argument_value, + 'join': join, + 'list_to_dict': list_to_dict, + } diff --git a/plugins/filter/split.yml b/plugins/filter/split.yml new file mode 100644 index 0000000..cb4ba88 --- /dev/null +++ b/plugins/filter/split.yml @@ -0,0 +1,33 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +DOCUMENTATION: + name: split + short_description: Split a command into arguments + version_added: 2.0.0 + description: + - Split a command into arguments. + options: + _input: + description: + - A command. + type: string + required: true + author: + - Felix Fontein (@felixfontein) + +EXAMPLES: | + --- + - name: Split command into list of arguments + ansible.builtin.set_fact: + argument_list: >- + {{ 'foo=bar comment="foo is bar" baz' | community.routeros.split }} + # Should result in ['foo=bar', 'comment=foo is bar', 'baz'] + +RETURN: + _value: + description: The list of arguments. + type: list + elements: string diff --git a/plugins/module_utils/__init__.py b/plugins/module_utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugins/module_utils/_api_data.py b/plugins/module_utils/_api_data.py new file mode 100644 index 0000000..f1d131d --- /dev/null +++ b/plugins/module_utils/_api_data.py @@ -0,0 +1,5442 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# The data inside here is private to this collection. If you use this from outside the collection, +# you are on your own. There can be random changes to its format even in bugfix releases! + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.community.routeros.plugins.module_utils.version import LooseVersion + + +def _compare(a, b, comparator): + if comparator == '==': + return a == b + if comparator == '!=': + return a != b + if comparator == '<': + return a < b + if comparator == '<=': + return a <= b + if comparator == '>': + return a > b + if comparator == '>=': + return a >= b + raise ValueError('Unknown comparator "{comparator}"'.format(comparator=comparator)) + + +class APIData(object): + def __init__(self, + unversioned=None, + versioned=None): + if (unversioned is None) == (versioned is None): + raise ValueError('either unversioned or versioned must be provided') + self.unversioned = unversioned + self.versioned = versioned + if self.unversioned is not None: + self.needs_version = self.unversioned.needs_version + self.fully_understood = self.unversioned.fully_understood + else: + self.needs_version = self.versioned is not None + # Mark as 'fully understood' if it is for at least one version + self.fully_understood = False + for dummy, dummy, unversioned in self.versioned: + if unversioned and not isinstance(unversioned, str) and unversioned.fully_understood: + self.fully_understood = True + break + self._current = None if self.needs_version else self.unversioned + + def _select(self, data, api_version): + if data is None: + self._current = None + return False, None + if isinstance(data, str): + self._current = None + return False, data + self._current = data.specialize_for_version(api_version) + return self._current.fully_understood, None + + def provide_version(self, version): + if not self.needs_version: + return self.unversioned.fully_understood, None + api_version = LooseVersion(version) + if self.unversioned is not None: + self._current = self.unversioned.specialize_for_version(api_version) + return self._current.fully_understood, None + for other_version, comparator, data in self.versioned: + if other_version == '*' and comparator == '*': + return self._select(data, api_version) + other_api_version = LooseVersion(other_version) + if _compare(api_version, other_api_version, comparator): + return self._select(data, api_version) + self._current = None + return False, None + + def get_data(self): + if self._current is None: + raise ValueError('either provide_version() was not called or it returned False') + return self._current + + +class VersionedAPIData(object): + def __init__(self, + primary_keys=None, + stratify_keys=None, + required_one_of=None, + mutually_exclusive=None, + has_identifier=False, + single_value=False, + unknown_mechanism=False, + fully_understood=False, + fixed_entries=False, + fields=None, + versioned_fields=None): + if sum([primary_keys is not None, stratify_keys is not None, has_identifier, single_value, unknown_mechanism]) > 1: + raise ValueError('primary_keys, stratify_keys, has_identifier, single_value, and unknown_mechanism are mutually exclusive') + if unknown_mechanism and fully_understood: + raise ValueError('unknown_mechanism and fully_understood cannot be combined') + self.primary_keys = primary_keys + self.stratify_keys = stratify_keys + self.required_one_of = required_one_of or [] + self.mutually_exclusive = mutually_exclusive or [] + self.has_identifier = has_identifier + self.single_value = single_value + self.unknown_mechanism = unknown_mechanism + self.fully_understood = fully_understood + self.fixed_entries = fixed_entries + if fixed_entries and primary_keys is None: + raise ValueError('fixed_entries can only be used with primary_keys') + if fields is None: + raise ValueError('fields must be provided') + self.fields = fields + if versioned_fields is not None: + if not isinstance(versioned_fields, list): + raise ValueError('unversioned_fields must be a list') + for conditions, name, field in versioned_fields: + if not isinstance(conditions, (tuple, list)): + raise ValueError('conditions must be a list or tuple') + if not isinstance(field, KeyInfo): + raise ValueError('field must be a KeyInfo object') + if name in fields: + raise ValueError('"{name}" appears both in fields and versioned_fields'.format(name=name)) + self.versioned_fields = versioned_fields or [] + if primary_keys: + for pk in primary_keys: + if pk not in fields: + raise ValueError('Primary key {pk} must be in fields!'.format(pk=pk)) + if stratify_keys: + for sk in stratify_keys: + if sk not in fields: + raise ValueError('Stratify key {sk} must be in fields!'.format(sk=sk)) + if required_one_of: + for index, require_list in enumerate(required_one_of): + if not isinstance(require_list, list): + raise ValueError('Require one of element at index #{index} must be a list!'.format(index=index + 1)) + for rk in require_list: + if rk not in fields: + raise ValueError('Require one of key {rk} must be in fields!'.format(rk=rk)) + if mutually_exclusive: + for index, exclusive_list in enumerate(mutually_exclusive): + if not isinstance(exclusive_list, list): + raise ValueError('Mutually exclusive element at index #{index} must be a list!'.format(index=index + 1)) + for ek in exclusive_list: + if ek not in fields: + raise ValueError('Mutually exclusive key {ek} must be in fields!'.format(ek=ek)) + self.needs_version = len(self.versioned_fields) > 0 + + def specialize_for_version(self, api_version): + fields = self.fields.copy() + for conditions, name, field in self.versioned_fields: + matching = True + for other_version, comparator in conditions: + other_api_version = LooseVersion(other_version) + if not _compare(api_version, other_api_version, comparator): + matching = False + break + if matching: + if name in fields: + raise ValueError( + 'Internal error: field "{field}" already exists for {version}'.format(field=name, version=api_version) + ) + fields[name] = field + return VersionedAPIData( + primary_keys=self.primary_keys, + stratify_keys=self.stratify_keys, + required_one_of=self.required_one_of, + mutually_exclusive=self.mutually_exclusive, + has_identifier=self.has_identifier, + single_value=self.single_value, + unknown_mechanism=self.unknown_mechanism, + fully_understood=self.fully_understood, + fixed_entries=self.fixed_entries, + fields=fields, + ) + + +class KeyInfo(object): + def __init__(self, + _dummy=None, + can_disable=False, + remove_value=None, + absent_value=None, + default=None, + required=False, + automatically_computed_from=None, + read_only=False, + write_only=False): + if _dummy is not None: + raise ValueError('KeyInfo() does not have positional arguments') + if sum([required, default is not None or can_disable, automatically_computed_from is not None]) > 1: + raise ValueError( + 'required, default, automatically_computed_from, and can_disable are mutually exclusive ' + 'besides default and can_disable which can be set together') + if not can_disable and remove_value is not None: + raise ValueError('remove_value can only be specified if can_disable=True') + if absent_value is not None and any([default is not None, automatically_computed_from is not None, can_disable]): + raise ValueError('absent_value can not be combined with default, automatically_computed_from, can_disable=True, or absent_value') + if read_only and write_only: + raise ValueError('read_only and write_only cannot be used at the same time') + if read_only and any([can_disable, remove_value is not None, absent_value is not None, default is not None, required]): + raise ValueError('read_only can not be combined with can_disable, remove_value, absent_value, default, or required') + self.can_disable = can_disable + self.remove_value = remove_value + self.automatically_computed_from = automatically_computed_from + self.default = default + self.required = required + self.absent_value = absent_value + self.read_only = read_only + self.write_only = write_only + + +def split_path(path): + return path.split() + + +def join_path(path): + return ' '.join(path) + + +# How to obtain this information: +# 1. Run `/export verbose` in the CLI; +# 2. All attributes listed there go into the `fields` list; +# attributes which can have a `!` ahead should have `canDisable=True` +# 3. All bold attributes go into the `primary_keys` list -- this is not always true! + +PATHS = { + ('interface', '6to4'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'clamp-tcp-mss': KeyInfo(default=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'dont-fragment': KeyInfo(default=False), + 'dscp': KeyInfo(default='inherit'), + 'ipsec-secret': KeyInfo(can_disable=True), + 'keepalive': KeyInfo(default='10s,10', can_disable=True), + 'local-address': KeyInfo(default='0.0.0.0'), + 'mtu': KeyInfo(default='auto'), + 'name': KeyInfo(), + 'remote-address': KeyInfo(required=True), + } + ), + ), + ('interface', 'bonding'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'arp': KeyInfo(default='enabled'), + 'arp-interval': KeyInfo(default='100ms'), + 'arp-ip-targets': KeyInfo(default=''), + 'arp-timeout': KeyInfo(default='auto'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'down-delay': KeyInfo(default='0ms'), + 'forced-mac-address': KeyInfo(can_disable=True), + 'lacp-rate': KeyInfo(default='30secs'), + 'lacp-user-key': KeyInfo(can_disable=True, remove_value=0), + 'link-monitoring': KeyInfo(default='mii'), + 'mii-interval': KeyInfo(default='100ms'), + 'min-links': KeyInfo(default=0), + 'mlag-id': KeyInfo(can_disable=True, remove_value=0), + 'mode': KeyInfo(default='balance-rr'), + 'mtu': KeyInfo(default=1500), + 'name': KeyInfo(), + 'primary': KeyInfo(default='none'), + 'slaves': KeyInfo(required=True), + 'transmit-hash-policy': KeyInfo(default='layer-2'), + 'up-delay': KeyInfo(default='0ms'), + } + ), + ), + ('interface', 'bridge'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + versioned_fields=[ + ([('7.0', '<')], 'ingress-filtering', KeyInfo(default=False)), + ([('7.0', '>=')], 'ingress-filtering', KeyInfo(default=True)), + ([('7.13', '>=')], 'port-cost-mode', KeyInfo(default='long')), + ([('7.16', '>=')], 'forward-reserved-addresses', KeyInfo(default=False)), + ([('7.16', '>=')], 'max-learned-entries', KeyInfo(default='auto')), + ], + fields={ + 'admin-mac': KeyInfo(default=''), + 'ageing-time': KeyInfo(default='5m'), + 'arp': KeyInfo(default='enabled'), + 'arp-timeout': KeyInfo(default='auto'), + 'auto-mac': KeyInfo(default=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'dhcp-snooping': KeyInfo(default=False), + 'disabled': KeyInfo(default=False), + 'ether-type': KeyInfo(default='0x8100'), + 'fast-forward': KeyInfo(default=True), + 'forward-delay': KeyInfo(default='15s'), + 'frame-types': KeyInfo(default='admit-all'), + 'igmp-snooping': KeyInfo(default=False), + 'max-message-age': KeyInfo(default='20s'), + 'mld-version': KeyInfo(default=1), + 'mtu': KeyInfo(default='auto'), + 'multicast-querier': KeyInfo(default=False), + 'name': KeyInfo(), + 'priority': KeyInfo(default='0x8000'), + 'protocol-mode': KeyInfo(default='rstp'), + 'pvid': KeyInfo(default=1), + 'transmit-hold-count': KeyInfo(default=6), + 'vlan-filtering': KeyInfo(default=False), + }, + ), + ), + ('interface', 'eoip'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + fields={ + 'allow-fast-path': KeyInfo(default=True), + 'arp': KeyInfo(default='enabled'), + 'arp-timeout': KeyInfo(default='auto'), + 'clamp-tcp-mss': KeyInfo(default=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'dont-fragment': KeyInfo(default=False), + 'dscp': KeyInfo(default='inherit'), + 'ipsec-secret': KeyInfo(can_disable=True), + 'keepalive': KeyInfo(default='10s,10', can_disable=True), + 'local-address': KeyInfo(default='0.0.0.0'), + 'loop-protect': KeyInfo(default='default'), + 'loop-protect-disable-time': KeyInfo(default='5m'), + 'loop-protect-send-interval': KeyInfo(default='5s'), + 'mac-address': KeyInfo(), + 'mtu': KeyInfo(default='auto'), + 'name': KeyInfo(), + 'remote-address': KeyInfo(required=True), + 'tunnel-id': KeyInfo(required=True), + }, + ), + ), + ('interface', 'ethernet'): APIData( + unversioned=VersionedAPIData( + fixed_entries=True, + fully_understood=True, + primary_keys=('default-name', ), + fields={ + 'default-name': KeyInfo(), + 'advertise': KeyInfo(), + 'arp': KeyInfo(default='enabled'), + 'arp-timeout': KeyInfo(default='auto'), + 'auto-negotiation': KeyInfo(default=True), + 'bandwidth': KeyInfo(default='unlimited/unlimited'), + 'combo-mode': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'fec-mode': KeyInfo(can_disable=True, remove_value='auto'), + 'full-duplex': KeyInfo(default=True), + 'l2mtu': KeyInfo(), + 'loop-protect': KeyInfo(default='default'), + 'loop-protect-disable-time': KeyInfo(default='5m'), + 'loop-protect-send-interval': KeyInfo(default='5s'), + 'mac-address': KeyInfo(), + 'mdix-enable': KeyInfo(), + 'mtu': KeyInfo(default=1500), + 'name': KeyInfo(), + 'orig-mac-address': KeyInfo(), + 'poe-out': KeyInfo(can_disable=True, remove_value='auto-on'), + 'poe-priority': KeyInfo(can_disable=True, remove_value=10), + 'poe-voltage': KeyInfo(can_disable=True), + 'power-cycle-interval': KeyInfo(), + 'power-cycle-ping-address': KeyInfo(can_disable=True), + 'power-cycle-ping-enabled': KeyInfo(), + 'power-cycle-ping-timeout': KeyInfo(can_disable=True), + 'rx-flow-control': KeyInfo(default='off'), + 'sfp-rate-select': KeyInfo(default='high'), + 'sfp-shutdown-temperature': KeyInfo(default=95), + 'speed': KeyInfo(), + 'tx-flow-control': KeyInfo(default='off'), + }, + ), + ), + ('interface', 'ethernet', 'poe'): APIData( + unversioned=VersionedAPIData( + fixed_entries=True, + fully_understood=True, + primary_keys=('name', ), + fields={ + 'name': KeyInfo(), + 'poe-out': KeyInfo(default='auto-on'), + 'poe-priority': KeyInfo(default=10), + 'poe-voltage': KeyInfo(default='auto'), + 'power-cycle-interval': KeyInfo(default='none'), + 'power-cycle-ping-address': KeyInfo(can_disable=True), + 'power-cycle-ping-enabled': KeyInfo(default=False), + 'power-cycle-ping-timeout': KeyInfo(can_disable=True), + } + ), + ), + ('interface', 'gre'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'allow-fast-path': KeyInfo(default=True), + 'clamp-tcp-mss': KeyInfo(default=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'dont-fragment': KeyInfo(default=False), + 'dscp': KeyInfo(default='inherit'), + 'ipsec-secret': KeyInfo(can_disable=True), + 'keepalive': KeyInfo(default='10s,10', can_disable=True), + 'local-address': KeyInfo(default='0.0.0.0'), + 'mtu': KeyInfo(default='auto'), + 'name': KeyInfo(), + 'remote-address': KeyInfo(required=True), + }, + ), + ), + ('interface', 'gre6'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + fields={ + 'clamp-tcp-mss': KeyInfo(default=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(default='inherit'), + 'ipsec-secret': KeyInfo(can_disable=True), + 'keepalive': KeyInfo(default='10s,10', can_disable=True), + 'local-address': KeyInfo(default='::'), + 'mtu': KeyInfo(default='auto'), + 'name': KeyInfo(), + 'remote-address': KeyInfo(required=True), + }, + ), + ), + ('interface', 'list'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'exclude': KeyInfo(default=''), + 'include': KeyInfo(default=''), + 'name': KeyInfo(), + }, + ), + ), + ('interface', 'list', 'member'): APIData( + unversioned=VersionedAPIData( + primary_keys=('list', 'interface', ), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'interface': KeyInfo(), + 'list': KeyInfo(), + 'disabled': KeyInfo(default=False), + }, + ), + ), + ('interface', 'lte', 'apn'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'add-default-route': KeyInfo(), + 'apn': KeyInfo(), + 'default-route-distance': KeyInfo(), + 'name': KeyInfo(), + 'use-peer-dns': KeyInfo(), + }, + ), + ), + ('interface', 'ppp-client'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'add-default-route': KeyInfo(default=True), + 'allow': KeyInfo(default='pap,chap,mschap1,mschap2'), + 'apn': KeyInfo(default='internet'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'data-channel': KeyInfo(default=0), + 'default-route-distance': KeyInfo(default=1), + 'dial-command': KeyInfo(default="ATDT"), + 'dial-on-demand': KeyInfo(default=True), + 'disabled': KeyInfo(default=True), + 'info-channel': KeyInfo(default=0), + 'keepalive-timeout': KeyInfo(default=30), + 'max-mru': KeyInfo(default=1500), + 'max-mtu': KeyInfo(default=1500), + 'modem-init': KeyInfo(default=''), + 'mrru': KeyInfo(default='disabled'), + 'name': KeyInfo(), + 'null-modem': KeyInfo(default=False), + 'password': KeyInfo(default=''), + 'phone': KeyInfo(default=''), + 'pin': KeyInfo(default=''), + 'port': KeyInfo(), + 'profile': KeyInfo(default='default'), + 'use-peer-dns': KeyInfo(default=True), + 'user': KeyInfo(default=''), + }, + ), + ), + ('interface', 'pppoe-client'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'ac-name': KeyInfo(default=''), + 'add-default-route': KeyInfo(default=False), + 'allow': KeyInfo(default='pap,chap,mschap1,mschap2'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'default-route-distance': KeyInfo(default=1), + 'dial-on-demand': KeyInfo(default=False), + 'disabled': KeyInfo(default=True), + 'host-uniq': KeyInfo(can_disable=True), + 'interface': KeyInfo(required=True), + 'keepalive-timeout': KeyInfo(default=10), + 'max-mru': KeyInfo(default='auto'), + 'max-mtu': KeyInfo(default='auto'), + 'mrru': KeyInfo(default='disabled'), + 'name': KeyInfo(), + 'password': KeyInfo(default=''), + 'profile': KeyInfo(default='default'), + 'service-name': KeyInfo(default=''), + 'use-peer-dns': KeyInfo(default=False), + 'user': KeyInfo(default=''), + }, + ), + ), + ('interface', 'vlan'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'arp': KeyInfo(default='enabled'), + 'arp-timeout': KeyInfo(default='auto'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(required=True), + 'loop-protect': KeyInfo(default='default'), + 'loop-protect-disable-time': KeyInfo(default='5m'), + 'loop-protect-send-interval': KeyInfo(default='5s'), + 'mtu': KeyInfo(default=1500), + 'name': KeyInfo(), + 'use-service-tag': KeyInfo(default=False), + 'vlan-id': KeyInfo(required=True), + }, + ), + ), + ('interface', 'vrrp'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'arp': KeyInfo(default='enabled'), + 'arp-timeout': KeyInfo(default='auto'), + 'authentication': KeyInfo(default='none'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'group-master': KeyInfo(default=''), + 'interface': KeyInfo(required=True), + 'interval': KeyInfo(default='1s'), + 'mtu': KeyInfo(default=1500), + 'name': KeyInfo(), + 'on-backup': KeyInfo(default=''), + 'on-fail': KeyInfo(default=''), + 'on-master': KeyInfo(default=''), + 'password': KeyInfo(default=''), + 'preemption-mode': KeyInfo(default=True), + 'priority': KeyInfo(default=100), + 'remote-address': KeyInfo(), + 'sync-connection-tracking': KeyInfo(default=False), + 'v3-protocol': KeyInfo(default='ipv4'), + 'version': KeyInfo(default=3), + 'vrid': KeyInfo(default=1), + }, + ), + ), + ('ip', 'hotspot', 'profile'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'dns-name': KeyInfo(), + 'hotspot-address': KeyInfo(), + 'html-directory': KeyInfo(), + 'html-directory-override': KeyInfo(), + 'http-cookie-lifetime': KeyInfo(), + 'http-proxy': KeyInfo(), + 'login-by': KeyInfo(), + 'name': KeyInfo(), + 'rate-limit': KeyInfo(), + 'smtp-server': KeyInfo(), + 'split-user-domain': KeyInfo(), + 'use-radius': KeyInfo(), + }, + ), + ), + ('ip', 'hotspot', 'user', 'profile'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'add-mac-cookie': KeyInfo(), + 'address-list': KeyInfo(), + 'idle-timeout': KeyInfo(), + 'insert-queue-before': KeyInfo(can_disable=True), + 'keepalive-timeout': KeyInfo(), + 'mac-cookie-timeout': KeyInfo(), + 'name': KeyInfo(), + 'parent-queue': KeyInfo(can_disable=True), + 'queue-type': KeyInfo(can_disable=True), + 'shared-users': KeyInfo(), + 'status-autorefresh': KeyInfo(), + 'transparent-proxy': KeyInfo(), + }, + ), + ), + ('ip', 'ipsec', 'identity'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('peer', ), + fields={ + 'auth-method': KeyInfo(default='pre-shared-key'), + 'certificate': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'eap-methods': KeyInfo(default='eap-tls'), + 'generate-policy': KeyInfo(default=False), + 'key': KeyInfo(), + 'match-by': KeyInfo(can_disable=True, remove_value='remote-id'), + 'mode-config': KeyInfo(can_disable=True, remove_value='none'), + 'my-id': KeyInfo(can_disable=True, remove_value='auto'), + 'notrack-chain': KeyInfo(can_disable=True, remove_value=''), + 'password': KeyInfo(), + 'peer': KeyInfo(), + 'policy-template-group': KeyInfo(can_disable=True, remove_value='default'), + 'remote-certificate': KeyInfo(), + 'remote-id': KeyInfo(can_disable=True, remove_value='auto'), + 'remote-key': KeyInfo(), + 'secret': KeyInfo(default=''), + 'username': KeyInfo(), + }, + ), + ), + ('ip', 'ipsec', 'mode-config'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + versioned_fields=[ + ([('6.43', '>=')], 'responder', KeyInfo(default=False)), + ([('6.44', '>=')], 'address', KeyInfo(can_disable=True, remove_value='0.0.0.0')), + ], + fields={ + 'address-pool': KeyInfo(can_disable=True, remove_value='none'), + 'address-prefix-length': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'name': KeyInfo(), + 'split-dns': KeyInfo(can_disable=True, remove_value=''), + 'split-include': KeyInfo(can_disable=True, remove_value=''), + 'src-address-list': KeyInfo(can_disable=True, remove_value=''), + 'static-dns': KeyInfo(can_disable=True, remove_value=''), + 'system-dns': KeyInfo(default=False), + }, + ), + ), + ('ip', 'ipsec', 'peer'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'address': KeyInfo(can_disable=True, remove_value=''), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'exchange-mode': KeyInfo(default='main'), + 'local-address': KeyInfo(can_disable=True, remove_value='0.0.0.0'), + 'name': KeyInfo(), + 'passive': KeyInfo(can_disable=True, remove_value=False), + 'port': KeyInfo(can_disable=True, remove_value=500), + 'profile': KeyInfo(default='default'), + 'send-initial-contact': KeyInfo(default=True), + }, + ), + ), + ('ip', 'ipsec', 'policy', 'group'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'name': KeyInfo(), + }, + ), + ), + ('ip', 'ipsec', 'profile'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'dh-group': KeyInfo(default='modp2048,modp1024'), + 'dpd-interval': KeyInfo(default='2m'), + 'dpd-maximum-failures': KeyInfo(default=5), + 'enc-algorithm': KeyInfo(default='aes-128,3des'), + 'hash-algorithm': KeyInfo(default='sha1'), + 'lifebytes': KeyInfo(can_disable=True, remove_value=0), + 'lifetime': KeyInfo(default='1d'), + 'name': KeyInfo(), + 'nat-traversal': KeyInfo(default=True), + 'prf-algorithm': KeyInfo(can_disable=True, remove_value='auto'), + 'proposal-check': KeyInfo(default='obey'), + }, + ), + ), + ('ip', 'ipsec', 'proposal'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'auth-algorithms': KeyInfo(default='sha1'), + 'disabled': KeyInfo(default=False), + 'enc-algorithms': KeyInfo(default='aes-256-cbc,aes-192-cbc,aes-128-cbc'), + 'lifetime': KeyInfo(default='30m'), + 'name': KeyInfo(), + 'pfs-group': KeyInfo(default='modp1024'), + }, + ), + ), + ('ip', 'pool'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'comment': KeyInfo(), + 'name': KeyInfo(), + 'next-pool': KeyInfo(), + 'ranges': KeyInfo(), + }, + ), + ), + ('ip', 'route'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'blackhole': KeyInfo(can_disable=True), + 'check-gateway': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'distance': KeyInfo(), + 'dst-address': KeyInfo(), + 'gateway': KeyInfo(), + 'pref-src': KeyInfo(), + 'routing-table': KeyInfo(default='main'), + 'route-tag': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'scope': KeyInfo(), + 'suppress-hw-offload': KeyInfo(default=False), + 'target-scope': KeyInfo(), + 'type': KeyInfo(can_disable=True, remove_value='unicast'), + 'vrf-interface': KeyInfo(can_disable=True), + }, + ), + ), + ('ip', 'route', 'rule'): APIData( + versioned=[ + ('7', '<', VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(default='lookup'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'dst-address': KeyInfo(can_disable=True), + 'interface': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'src-address': KeyInfo(can_disable=True), + 'table': KeyInfo(default='main'), + }, + )), + ], + ), + ('ip', 'vrf'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interfaces': KeyInfo(), + 'name': KeyInfo(), + }, + )), + ] + ), + ('ip', 'route', 'vrf'): APIData( + versioned=[ + ('7', '<', VersionedAPIData( + fully_understood=True, + primary_keys=('routing-mark', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interfaces': KeyInfo(), + 'routing-mark': KeyInfo(), + }, + )), + ], + ), + ('routing', 'filter'): APIData( + versioned=[ + ('7', '<', VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(default='passthrough'), + 'address-family': KeyInfo(can_disable=True), + 'append-bgp-communities': KeyInfo(can_disable=True), + 'append-route-targets': KeyInfo(can_disable=True), + 'bgp-as-path': KeyInfo(can_disable=True), + 'bgp-as-path-length': KeyInfo(can_disable=True), + 'bgp-atomic-aggregate': KeyInfo(can_disable=True), + 'bgp-communities': KeyInfo(can_disable=True), + 'bgp-local-pref': KeyInfo(can_disable=True), + 'bgp-med': KeyInfo(can_disable=True), + 'bgp-origin': KeyInfo(can_disable=True), + 'bgp-weight': KeyInfo(can_disable=True), + 'chain': KeyInfo(required=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'distance': KeyInfo(can_disable=True), + 'invert-match': KeyInfo(default=False), + 'jump-target': KeyInfo(), + 'locally-originated-bgp': KeyInfo(can_disable=True), + 'match-chain': KeyInfo(can_disable=True), + 'ospf-type': KeyInfo(can_disable=True), + 'pref-src': KeyInfo(can_disable=True), + 'prefix': KeyInfo(default='0.0.0.0/0'), + 'prefix-length': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'route-comment': KeyInfo(can_disable=True), + 'route-tag': KeyInfo(can_disable=True), + 'route-targets': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'scope': KeyInfo(can_disable=True), + 'set-bgp-communities': KeyInfo(can_disable=True), + 'set-bgp-local-pref': KeyInfo(can_disable=True), + 'set-bgp-med': KeyInfo(can_disable=True), + 'set-bgp-prepend': KeyInfo(can_disable=True), + 'set-bgp-prepend-path': KeyInfo(), + 'set-bgp-weight': KeyInfo(can_disable=True), + 'set-check-gateway': KeyInfo(can_disable=True), + 'set-disabled': KeyInfo(can_disable=True), + 'set-distance': KeyInfo(can_disable=True), + 'set-in-nexthop': KeyInfo(can_disable=True), + 'set-in-nexthop-direct': KeyInfo(can_disable=True), + 'set-in-nexthop-ipv6': KeyInfo(can_disable=True), + 'set-in-nexthop-linklocal': KeyInfo(can_disable=True), + 'set-out-nexthop': KeyInfo(can_disable=True), + 'set-out-nexthop-ipv6': KeyInfo(can_disable=True), + 'set-out-nexthop-linklocal': KeyInfo(can_disable=True), + 'set-pref-src': KeyInfo(can_disable=True), + 'set-route-comment': KeyInfo(can_disable=True), + 'set-route-tag': KeyInfo(can_disable=True), + 'set-route-targets': KeyInfo(can_disable=True), + 'set-routing-mark': KeyInfo(can_disable=True), + 'set-scope': KeyInfo(can_disable=True), + 'set-site-of-origin': KeyInfo(can_disable=True), + 'set-target-scope': KeyInfo(can_disable=True), + 'set-type': KeyInfo(can_disable=True), + 'set-use-te-nexthop': KeyInfo(can_disable=True), + 'site-of-origin': KeyInfo(can_disable=True), + 'target-scope': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('routing', 'filter', 'num-list'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'list': KeyInfo(required=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(can_disable=True), + 'range': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('routing', 'filter', 'rule'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'chain': KeyInfo(required=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(can_disable=True), + 'rule': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('routing', 'filter', 'select-rule'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'chain': KeyInfo(required=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(can_disable=True), + 'do-group-num': KeyInfo(can_disable=True), + 'do-group-prfx': KeyInfo(can_disable=True), + 'do-jump': KeyInfo(can_disable=True), + 'do-select-num': KeyInfo(can_disable=True), + 'do-select-prfx': KeyInfo(can_disable=True), + 'do-take': KeyInfo(can_disable=True), + 'do-where': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('routing', 'filter', 'community-list'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'list': KeyInfo(required=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(can_disable=True), + 'communities': KeyInfo(can_disable=True), + 'regexp': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('routing', 'ospf', 'instance'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'domain-id': KeyInfo(can_disable=True), + 'domain-tag': KeyInfo(can_disable=True), + 'in-filter-chain': KeyInfo(can_disable=True), + 'mpls-te-address': KeyInfo(can_disable=True), + 'mpls-te-area': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'originate-default': KeyInfo(can_disable=True), + 'out-filter-chain': KeyInfo(can_disable=True), + 'out-filter-select': KeyInfo(can_disable=True), + 'redistribute': KeyInfo(can_disable=True), + 'router-id': KeyInfo(default='main'), + 'routing-table': KeyInfo(can_disable=True), + 'use-dn': KeyInfo(can_disable=True), + 'version': KeyInfo(default=2), + 'vrf': KeyInfo(default='main'), + }, + ), + ), + ('routing', 'ospf', 'area'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'area-id': KeyInfo(default='0.0.0.0'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'default-cost': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'instance': KeyInfo(required=True), + 'name': KeyInfo(), + 'no-summaries': KeyInfo(can_disable=True), + 'nssa-translator': KeyInfo(can_disable=True), + 'type': KeyInfo(default='default'), + }, + ), + ), + ('routing', 'ospf', 'area', 'range'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('area', 'prefix', ), + fields={ + 'advertise': KeyInfo(default=True), + 'area': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'cost': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'prefix': KeyInfo(), + }, + ), + ), + ('routing', 'ospf', 'interface-template'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'area': KeyInfo(required=True), + 'auth': KeyInfo(can_disable=True), + 'auth-id': KeyInfo(can_disable=True), + 'auth-key': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'cost': KeyInfo(default=1), + 'dead-interval': KeyInfo(default='40s'), + 'disabled': KeyInfo(default=False), + 'hello-interval': KeyInfo(default='10s'), + 'instance-id': KeyInfo(default=0), + 'interfaces': KeyInfo(can_disable=True), + 'networks': KeyInfo(can_disable=True), + 'passive': KeyInfo(can_disable=True), + 'prefix-list': KeyInfo(can_disable=True), + 'priority': KeyInfo(default=128), + 'retransmit-interval': KeyInfo(default='5s'), + 'transmit-delay': KeyInfo(default='1s'), + 'type': KeyInfo(default='broadcast'), + 'vlink-neighbor-id': KeyInfo(can_disable=True), + 'vlink-transit-area': KeyInfo(can_disable=True), + }, + ), + ), + ('routing', 'ospf', 'static-neighbor'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'address': KeyInfo(required=True), + 'area': KeyInfo(required=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'instance-id': KeyInfo(default=0), + 'poll-interval': KeyInfo(default='2m'), + }, + )), + ], + ), + ('routing', 'ospf-v3', 'instance'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'disabled': KeyInfo(), + 'distribute-default': KeyInfo(), + 'metric-bgp': KeyInfo(), + 'metric-connected': KeyInfo(), + 'metric-default': KeyInfo(), + 'metric-other-ospf': KeyInfo(), + 'metric-rip': KeyInfo(), + 'metric-static': KeyInfo(), + 'name': KeyInfo(), + 'redistribute-bgp': KeyInfo(), + 'redistribute-connected': KeyInfo(), + 'redistribute-other-ospf': KeyInfo(), + 'redistribute-rip': KeyInfo(), + 'redistribute-static': KeyInfo(), + 'router-id': KeyInfo(), + }, + ), + ), + ('routing', 'ospf-v3', 'area'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'area-id': KeyInfo(), + 'disabled': KeyInfo(), + 'instance': KeyInfo(), + 'name': KeyInfo(), + 'type': KeyInfo(), + }, + ), + ), + ('routing', 'pimsm', 'instance'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'afi': KeyInfo(default='ipv4'), + 'bsm-forward-back': KeyInfo(), + 'crp-advertise-contained': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + 'rp-hash-mask-length': KeyInfo(), + 'rp-static-override': KeyInfo(default=False), + 'ssm-range': KeyInfo(), + 'switch-to-spt': KeyInfo(default=True), + 'switch-to-spt-bytes': KeyInfo(default=0), + 'switch-to-spt-interval': KeyInfo(), + 'vrf': KeyInfo(default="main"), + }, + ), + ), + ('routing', 'pimsm', 'interface-template'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'disabled': KeyInfo(default=False), + 'hello-delay': KeyInfo(default='5s'), + 'hello-period': KeyInfo(default='30s'), + 'instance': KeyInfo(required=True), + 'interfaces': KeyInfo(can_disable=True), + 'join-prune-period': KeyInfo(default='1m'), + 'join-tracking-support': KeyInfo(default=True), + 'override-interval': KeyInfo(default='2s500ms'), + 'priority': KeyInfo(default=1), + 'propagation-delay': KeyInfo(default='500ms'), + 'source-addresses': KeyInfo(can_disable=True), + }, + ), + ), + ('routing', 'rule'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'dst-address': KeyInfo(can_disable=True), + 'interface': KeyInfo(can_disable=True), + 'min-prefix': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'src-address': KeyInfo(can_disable=True), + 'table': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('routing', 'table'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'name': KeyInfo(required=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'fib': KeyInfo(), + }, + )), + ], + ), + ('snmp', 'community'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'default': KeyInfo(read_only=True), + 'addresses': KeyInfo(default='::/0'), + 'authentication-password': KeyInfo(default=''), + 'authentication-protocol': KeyInfo(default='MD5'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'encryption-password': KeyInfo(default=''), + 'encryption-protocol': KeyInfo(default='DES'), + 'name': KeyInfo(required=True), + 'read-access': KeyInfo(default=True), + 'security': KeyInfo(default='none'), + 'write-access': KeyInfo(default=False), + }, + ), + ), + ('caps-man', 'aaa'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'called-format': KeyInfo(default='mac:ssid'), + 'interim-update': KeyInfo(default='disabled'), + 'mac-caching': KeyInfo(default='disabled'), + 'mac-format': KeyInfo(default='XX:XX:XX:XX:XX:XX'), + 'mac-mode': KeyInfo(default='as-username'), + }, + ), + ), + ('caps-man', 'access-list'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(can_disable=True), + 'allow-signal-out-of-range': KeyInfo(can_disable=True), + 'ap-tx-limit': KeyInfo(can_disable=True), + 'client-to-client-forwarding': KeyInfo(can_disable=True), + 'client-tx-limit': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(can_disable=True), + 'mac-address': KeyInfo(can_disable=True), + 'mac-address-mask': KeyInfo(can_disable=True), + 'private-passphrase': KeyInfo(can_disable=True), + 'radius-accounting': KeyInfo(can_disable=True), + 'signal-range': KeyInfo(can_disable=True), + 'ssid-regexp': KeyInfo(), + 'time': KeyInfo(can_disable=True), + 'vlan-id': KeyInfo(can_disable=True), + 'vlan-mode': KeyInfo(can_disable=True), + }, + ), + ), + ('caps-man', 'channel'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'band': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'control-channel-width': KeyInfo(can_disable=True), + 'extension-channel': KeyInfo(can_disable=True), + 'frequency': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'reselect-interval': KeyInfo(can_disable=True), + 'save-selected': KeyInfo(can_disable=True), + 'secondary-frequency': KeyInfo(can_disable=True), + 'skip-dfs-channels': KeyInfo(can_disable=True), + 'tx-power': KeyInfo(can_disable=True), + }, + ), + ), + ('caps-man', 'configuration'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'channel': KeyInfo(can_disable=True), + 'channel.band': KeyInfo(can_disable=True), + 'channel.control-channel-width': KeyInfo(can_disable=True), + 'channel.extension-channel': KeyInfo(can_disable=True), + 'channel.frequency': KeyInfo(can_disable=True), + 'channel.reselect-interval': KeyInfo(can_disable=True), + 'channel.save-selected': KeyInfo(can_disable=True), + 'channel.secondary-frequency': KeyInfo(can_disable=True), + 'channel.skip-dfs-channels': KeyInfo(can_disable=True), + 'channel.tx-power': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'country': KeyInfo(can_disable=True), + 'datapath': KeyInfo(can_disable=True), + 'datapath.arp': KeyInfo(), + 'datapath.bridge': KeyInfo(can_disable=True), + 'datapath.bridge-cost': KeyInfo(can_disable=True), + 'datapath.bridge-horizon': KeyInfo(can_disable=True), + 'datapath.client-to-client-forwarding': KeyInfo(can_disable=True), + 'datapath.interface-list': KeyInfo(can_disable=True), + 'datapath.l2mtu': KeyInfo(), + 'datapath.local-forwarding': KeyInfo(can_disable=True), + 'datapath.mtu': KeyInfo(), + 'datapath.openflow-switch': KeyInfo(can_disable=True), + 'datapath.vlan-id': KeyInfo(can_disable=True), + 'datapath.vlan-mode': KeyInfo(can_disable=True), + 'disconnect-timeout': KeyInfo(can_disable=True), + 'distance': KeyInfo(can_disable=True), + 'frame-lifetime': KeyInfo(can_disable=True), + 'guard-interval': KeyInfo(can_disable=True), + 'hide-ssid': KeyInfo(can_disable=True), + 'hw-protection-mode': KeyInfo(can_disable=True), + 'hw-retries': KeyInfo(can_disable=True), + 'installation': KeyInfo(can_disable=True), + 'keepalive-frames': KeyInfo(can_disable=True), + 'load-balancing-group': KeyInfo(can_disable=True), + 'max-sta-count': KeyInfo(can_disable=True), + 'mode': KeyInfo(can_disable=True), + 'multicast-helper': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'rates': KeyInfo(can_disable=True), + 'rates.basic': KeyInfo(can_disable=True), + 'rates.ht-basic-mcs': KeyInfo(can_disable=True), + 'rates.ht-supported-mcs': KeyInfo(can_disable=True), + 'rates.supported': KeyInfo(can_disable=True), + 'rates.vht-basic-mcs': KeyInfo(can_disable=True), + 'rates.vht-supported-mcs': KeyInfo(can_disable=True), + 'rx-chains': KeyInfo(can_disable=True), + 'security': KeyInfo(can_disable=True), + 'security.authentication-types': KeyInfo(can_disable=True), + 'security.disable-pmkid': KeyInfo(can_disable=True), + 'security.eap-methods': KeyInfo(can_disable=True), + 'security.eap-radius-accounting': KeyInfo(can_disable=True), + 'security.encryption': KeyInfo(can_disable=True), + 'security.group-encryption': KeyInfo(can_disable=True), + 'security.group-key-update': KeyInfo(), + 'security.passphrase': KeyInfo(can_disable=True), + 'security.tls-certificate': KeyInfo(), + 'security.tls-mode': KeyInfo(), + 'ssid': KeyInfo(can_disable=True), + 'tx-chains': KeyInfo(can_disable=True), + }, + ), + ), + ('caps-man', 'datapath'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'arp': KeyInfo(), + 'bridge': KeyInfo(can_disable=True), + 'bridge-cost': KeyInfo(can_disable=True), + 'bridge-horizon': KeyInfo(can_disable=True), + 'client-to-client-forwarding': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'interface-list': KeyInfo(can_disable=True), + 'l2mtu': KeyInfo(), + 'local-forwarding': KeyInfo(can_disable=True), + 'mtu': KeyInfo(), + 'name': KeyInfo(), + 'openflow-switch': KeyInfo(can_disable=True), + 'vlan-id': KeyInfo(can_disable=True), + 'vlan-mode': KeyInfo(can_disable=True), + }, + ), + ), + ('caps-man', 'manager', 'interface'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('interface', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'default': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'forbid': KeyInfo(default=False), + 'interface': KeyInfo(), + }, + ), + ), + ('caps-man', 'provisioning'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(default='none'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'common-name-regexp': KeyInfo(default=''), + 'disabled': KeyInfo(default=False), + 'hw-supported-modes': KeyInfo(default=''), + 'identity-regexp': KeyInfo(default=''), + 'ip-address-ranges': KeyInfo(default=''), + 'master-configuration': KeyInfo(default='*FFFFFFFF'), + 'name-format': KeyInfo(default='cap'), + 'name-prefix': KeyInfo(default=''), + 'radio-mac': KeyInfo(default='00:00:00:00:00:00'), + 'slave-configurations': KeyInfo(default=''), + }, + ), + ), + ('caps-man', 'security'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'authentication-types': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disable-pmkid': KeyInfo(can_disable=True), + 'eap-methods': KeyInfo(can_disable=True), + 'eap-radius-accounting': KeyInfo(can_disable=True), + 'encryption': KeyInfo(can_disable=True), + 'group-encryption': KeyInfo(can_disable=True), + 'group-key-update': KeyInfo(), + 'name': KeyInfo(), + 'passphrase': KeyInfo(can_disable=True), + 'tls-certificate': KeyInfo(), + 'tls-mode': KeyInfo(), + } + ), + ), + ('certificate', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'crl-download': KeyInfo(default=False), + 'crl-store': KeyInfo(default='ram'), + 'crl-use': KeyInfo(default=False), + }, + ), + ), + ('interface', 'bridge', 'port'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('interface', ), + versioned_fields=[ + ([('7.0', '<')], 'ingress-filtering', KeyInfo(default=False)), + ([('7.0', '>=')], 'ingress-filtering', KeyInfo(default=True)), + ], + fields={ + 'auto-isolate': KeyInfo(default=False), + 'bpdu-guard': KeyInfo(default=False), + 'bridge': KeyInfo(required=True), + 'broadcast-flood': KeyInfo(default=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'edge': KeyInfo(default='auto'), + 'fast-leave': KeyInfo(default=False), + 'frame-types': KeyInfo(default='admit-all'), + 'horizon': KeyInfo(default='none'), + 'hw': KeyInfo(default=True), + 'interface': KeyInfo(), + 'internal-path-cost': KeyInfo(default=10), + 'learn': KeyInfo(default='auto'), + 'multicast-router': KeyInfo(default='temporary-query'), + 'path-cost': KeyInfo(default=10), + 'point-to-point': KeyInfo(default='auto'), + 'priority': KeyInfo(default='0x80'), + 'pvid': KeyInfo(default=1), + 'restricted-role': KeyInfo(default=False), + 'restricted-tcn': KeyInfo(default=False), + 'tag-stacking': KeyInfo(default=False), + 'trusted': KeyInfo(default=False), + 'unknown-multicast-flood': KeyInfo(default=True), + 'unknown-unicast-flood': KeyInfo(default=True), + }, + ), + ), + ('interface', 'bridge', 'mlag'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'bridge': KeyInfo(default='none'), + 'peer-port': KeyInfo(default='none'), + } + ), + ), + ('interface', 'bridge', 'port-controller'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'bridge': KeyInfo(default='none'), + 'cascade-ports': KeyInfo(default=''), + 'switch': KeyInfo(default='none'), + }, + ), + ), + ('interface', 'bridge', 'port-extender'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'control-ports': KeyInfo(default=''), + 'excluded-ports': KeyInfo(default=''), + 'switch': KeyInfo(default='none'), + }, + ), + ), + ('interface', 'bridge', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allow-fast-path': KeyInfo(default=True), + 'use-ip-firewall': KeyInfo(default=False), + 'use-ip-firewall-for-pppoe': KeyInfo(default=False), + 'use-ip-firewall-for-vlan': KeyInfo(default=False), + }, + ), + ), + ('interface', 'bridge', 'vlan'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('bridge', 'vlan-ids', ), + fields={ + 'bridge': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'tagged': KeyInfo(default=''), + 'untagged': KeyInfo(default=''), + 'vlan-ids': KeyInfo(), + }, + ), + ), + ('ip', 'firewall', 'connection', 'tracking'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'enabled': KeyInfo(default='auto'), + 'generic-timeout': KeyInfo(default='10m'), + 'icmp-timeout': KeyInfo(default='10s'), + 'loose-tcp-tracking': KeyInfo(default=True), + 'tcp-close-timeout': KeyInfo(default='10s'), + 'tcp-close-wait-timeout': KeyInfo(default='10s'), + 'tcp-established-timeout': KeyInfo(default='1d'), + 'tcp-fin-wait-timeout': KeyInfo(default='10s'), + 'tcp-last-ack-timeout': KeyInfo(default='10s'), + 'tcp-max-retrans-timeout': KeyInfo(default='5m'), + 'tcp-syn-received-timeout': KeyInfo(default='5s'), + 'tcp-syn-sent-timeout': KeyInfo(default='5s'), + 'tcp-time-wait-timeout': KeyInfo(default='10s'), + 'tcp-unacked-timeout': KeyInfo(default='5m'), + 'udp-stream-timeout': KeyInfo(default='3m'), + 'udp-timeout': KeyInfo(default='10s'), + }, + ), + ), + ('ip', 'neighbor', 'discovery-settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + versioned_fields=[ + ([('7.7', '>=')], 'mode', KeyInfo(default='tx-and-rx')), + ([('7.15', '>=')], 'lldp-mac-phy-config', KeyInfo(default=False)), + ([('7.16', '>=')], 'discover-interval', KeyInfo(default='30s')), + ([('7.16', '>=')], 'lldp-vlan-info', KeyInfo(default=False)), + ], + fields={ + 'discover-interface-list': KeyInfo(), + 'lldp-med-net-policy-vlan': KeyInfo(default='disabled'), + 'protocol': KeyInfo(default='cdp,lldp,mndp'), + }, + ), + ), + ('ip', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + versioned_fields=[ + ([('7.16', '>=')], 'ipv4-multipath-hash-policy', KeyInfo(default='l3')), + ], + fields={ + 'accept-redirects': KeyInfo(default=False), + 'accept-source-route': KeyInfo(default=False), + 'allow-fast-path': KeyInfo(default=True), + 'arp-timeout': KeyInfo(default='30s'), + 'icmp-rate-limit': KeyInfo(default=10), + 'icmp-rate-mask': KeyInfo(default='0x1818'), + 'ip-forward': KeyInfo(default=True), + 'max-neighbor-entries': KeyInfo(default=8192), + 'route-cache': KeyInfo(default=True), + 'rp-filter': KeyInfo(default=False), + 'secure-redirects': KeyInfo(default=True), + 'send-redirects': KeyInfo(default=True), + 'tcp-syncookies': KeyInfo(default=False), + }, + ), + ), + ('ipv6', 'address'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'address': KeyInfo(), + 'advertise': KeyInfo(default=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'eui-64': KeyInfo(default=False), + 'from-pool': KeyInfo(default=''), + 'interface': KeyInfo(required=True), + 'no-dad': KeyInfo(default=False), + }, + ), + ), + ('ipv6', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + versioned_fields=[ + ([('7.16', '>=')], 'multipath-hash-policy', KeyInfo(default='l3')), + ([('7.17', '>=')], 'disable-link-local-address', KeyInfo(default=False)), + ([('7.17', '>=')], 'stale-neighbor-timeout', KeyInfo(default=60)), + ([('7.18', '>=')], 'allow-fast-path', KeyInfo(default=True)), + ([('7.18', '<')], 'max-neighbor-entries', KeyInfo(default=8192)), + ([('7.18', '>=')], 'min-neighbor-entries', KeyInfo()), + ([('7.18', '>=')], 'soft-max-neighbor-entries', KeyInfo()), + ([('7.18', '>=')], 'max-neighbor-entries', KeyInfo()), + ], + fields={ + 'accept-redirects': KeyInfo(default='yes-if-forwarding-disabled'), + 'accept-router-advertisements': KeyInfo(default='yes-if-forwarding-disabled'), + 'disable-ipv6': KeyInfo(default=False), + 'forward': KeyInfo(default=True), + }, + ), + ), + ('interface', 'detect-internet'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'detect-interface-list': KeyInfo(default='none'), + 'internet-interface-list': KeyInfo(default='none'), + 'lan-interface-list': KeyInfo(default='none'), + 'wan-interface-list': KeyInfo(default='none'), + }, + ), + ), + ('interface', 'l2tp-client',): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + single_value=False, + fully_understood=True, + fields={ + 'add-default-route': KeyInfo(default=False), + 'allow': KeyInfo(default='pap,chap,mschap1,mschap2'), + 'allow-fast-path': KeyInfo(default=False), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connect-to': KeyInfo(required=True), + 'default-route-distance': KeyInfo(default=False), + 'dial-on-demand': KeyInfo(default=False), + 'disabled': KeyInfo(default=True), + 'ipsec-secret': KeyInfo(default=''), + 'keepalive-timeout': KeyInfo(default=60), + 'l2tp-proto-version': KeyInfo(default='l2tpv2'), + 'l2tpv3-cookie-length': KeyInfo(default=0), + 'l2tpv3-digest-hash': KeyInfo(default='md5'), + 'max-mru': KeyInfo(default=1450), + 'max-mtu': KeyInfo(default=1450), + 'mrru': KeyInfo(default='disabled'), + 'name': KeyInfo(required=True), + 'password': KeyInfo(), + 'profile': KeyInfo(default='default-encryption'), + 'src-address': KeyInfo(), + 'use-ipsec': KeyInfo(default=False), + 'use-peer-dns': KeyInfo(default=False), + 'user': KeyInfo(required=True), + }, + ), + ), + ('interface', 'l2tp-server', 'server'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allow-fast-path': KeyInfo(default=False), + 'authentication': KeyInfo(default='pap,chap,mschap1,mschap2'), + 'caller-id-type': KeyInfo(default='ip-address'), + 'default-profile': KeyInfo(default='default-encryption'), + 'enabled': KeyInfo(default=False), + 'ipsec-secret': KeyInfo(default=''), + 'keepalive-timeout': KeyInfo(default=30), + 'max-mru': KeyInfo(default=1450), + 'max-mtu': KeyInfo(default=1450), + 'max-sessions': KeyInfo(default='unlimited'), + 'mrru': KeyInfo(default='disabled'), + 'one-session-per-host': KeyInfo(default=False), + 'use-ipsec': KeyInfo(default=False), + }, + ), + ), + ('interface', 'ovpn-client'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'add-default-route': KeyInfo(default=False), + 'auth': KeyInfo(default='sha1'), + 'certificate': KeyInfo(), + 'cipher': KeyInfo(default='blowfish128'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connect-to': KeyInfo(), + 'disabled': KeyInfo(default=True), + 'disconnect-notify': KeyInfo(), + 'mac-address': KeyInfo(), + 'max-mtu': KeyInfo(default=1500), + 'mode': KeyInfo(default='ip'), + 'name': KeyInfo(), + 'password': KeyInfo(), + 'port': KeyInfo(default=1194), + 'profile': KeyInfo(default='default'), + 'protocol': KeyInfo(default='tcp'), + 'route-nopull': KeyInfo(default=False), + 'tls-version': KeyInfo(default='any'), + 'use-peer-dns': KeyInfo(default=True), + 'user': KeyInfo(), + 'verify-server-certificate': KeyInfo(default=False), + }, + ), + ), + ('interface', 'ovpn-server', 'server'): APIData( + versioned=[ + ('7.17', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'auth': KeyInfo(), + 'cipher': KeyInfo(), + 'default-profile': KeyInfo(default='default'), + 'enabled': KeyInfo(default=False), + 'keepalive-timeout': KeyInfo(default=60), + 'mac-address': KeyInfo(), + 'max-mtu': KeyInfo(default=1500), + 'mode': KeyInfo(default='ip'), + 'name': KeyInfo(default=''), + 'netmask': KeyInfo(default=24), + 'port': KeyInfo(default=1194), + 'protocol': KeyInfo(default='tcp'), + 'require-client-certificate': KeyInfo(default=False), + 'vrf': KeyInfo(default='main'), + }, + )), + ('7.17', '<', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'auth': KeyInfo(), + 'cipher': KeyInfo(), + 'default-profile': KeyInfo(default='default'), + 'enabled': KeyInfo(default=False), + 'keepalive-timeout': KeyInfo(default=60), + 'mac-address': KeyInfo(), + 'max-mtu': KeyInfo(default=1500), + 'mode': KeyInfo(default='ip'), + 'name': KeyInfo(default=''), + 'netmask': KeyInfo(default=24), + 'port': KeyInfo(default=1194), + 'protocol': KeyInfo(default='tcp'), + 'require-client-certificate': KeyInfo(default=False), + }, + )) + ] + ), + ('interface', 'pppoe-server', 'server'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('interface', ), + fields={ + 'accept-empty-service': KeyInfo(default=True), + 'authentication': KeyInfo(default='pap,chap,mschap1,mschap2'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'default-profile': KeyInfo(default='default'), + 'disabled': KeyInfo(default=True), + 'interface': KeyInfo(required=True), + 'keepalive-timeout': KeyInfo(default=10), + 'max-mru': KeyInfo(default='auto'), + 'max-mtu': KeyInfo(default='auto'), + 'max-sessions': KeyInfo(default='unlimited'), + 'mrru': KeyInfo(default='disabled'), + 'one-session-per-host': KeyInfo(default=False), + 'pado-delay': KeyInfo(default=0), + 'service-name': KeyInfo(default=''), + }, + ), + ), + ('interface', 'pptp-server', 'server'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'authentication': KeyInfo(default='mschap1,mschap2'), + 'default-profile': KeyInfo(default='default-encryption'), + 'enabled': KeyInfo(default=False), + 'keepalive-timeout': KeyInfo(default=30), + 'max-mru': KeyInfo(default=1450), + 'max-mtu': KeyInfo(default=1450), + 'mrru': KeyInfo(default='disabled'), + }, + ), + ), + ('interface', 'sstp-server', 'server'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'authentication': KeyInfo(default='pap,chap,mschap1,mschap2'), + 'certificate': KeyInfo(default='none'), + 'default-profile': KeyInfo(default='default'), + 'enabled': KeyInfo(default=False), + 'force-aes': KeyInfo(default=False), + 'keepalive-timeout': KeyInfo(default=60), + 'max-mru': KeyInfo(default=1500), + 'max-mtu': KeyInfo(default=1500), + 'mrru': KeyInfo(default='disabled'), + 'pfs': KeyInfo(default=False), + 'port': KeyInfo(default=443), + 'tls-version': KeyInfo(default='any'), + 'verify-client-certificate': KeyInfo(default='no'), + }, + ), + ), + ('interface', 'wifi'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + required_one_of=[['default-name', 'radio-mac', 'master-interface']], + fields={ + 'aaa.called-format': KeyInfo(can_disable=True), + 'aaa.calling-format': KeyInfo(can_disable=True), + 'aaa.interim-update': KeyInfo(can_disable=True), + 'aaa.mac-caching': KeyInfo(can_disable=True), + 'aaa.nas-identifier': KeyInfo(can_disable=True), + 'aaa.password-format': KeyInfo(can_disable=True), + 'aaa.username-format': KeyInfo(can_disable=True), + 'aaa': KeyInfo(can_disable=True), + 'arp-timeout': KeyInfo(default='auto'), + 'arp': KeyInfo(can_disable=True), + 'channel.band': KeyInfo(can_disable=True), + 'channel.frequency': KeyInfo(can_disable=True), + 'channel.secondary-frequency': KeyInfo(can_disable=True), + 'channel.skip-dfs-channels': KeyInfo(can_disable=True), + 'channel.width': KeyInfo(can_disable=True), + 'channel': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'configuration.antenna-gain': KeyInfo(can_disable=True), + 'configuration.beacon-interval': KeyInfo(can_disable=True), + 'configuration.chains': KeyInfo(can_disable=True), + 'configuration.country': KeyInfo(can_disable=True), + 'configuration.dtim-period': KeyInfo(can_disable=True), + 'configuration.hide-ssid': KeyInfo(can_disable=True), + 'configuration.manager': KeyInfo(can_disable=True), + 'configuration.mode': KeyInfo(can_disable=True), + 'configuration.multicast-enhance': KeyInfo(can_disable=True), + 'configuration.qos-classifier': KeyInfo(can_disable=True), + 'configuration.ssid': KeyInfo(can_disable=True), + 'configuration.tx-chain': KeyInfo(can_disable=True), + 'configuration.tx-power': KeyInfo(can_disable=True), + 'configuration': KeyInfo(can_disable=True), + 'datapath.bridge-cost': KeyInfo(can_disable=True), + 'datapath.bridge-horizon': KeyInfo(can_disable=True), + 'datapath.bridge': KeyInfo(can_disable=True), + 'datapath.client-isolation': KeyInfo(can_disable=True), + 'datapath.interface-list': KeyInfo(can_disable=True), + 'datapath.vlan-id': KeyInfo(can_disable=True), + 'datapath': KeyInfo(can_disable=True), + 'default-name': KeyInfo(), + 'disable-running-check': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=True), + 'interworking.3gpp-info': KeyInfo(can_disable=True), + 'interworking.authentication-types': KeyInfo(can_disable=True), + 'interworking.connection-capabilities': KeyInfo(can_disable=True), + 'interworking.domain-names': KeyInfo(can_disable=True), + 'interworking.esr': KeyInfo(can_disable=True), + 'interworking.hessid': KeyInfo(can_disable=True), + 'interworking.hotspot20-dgaf': KeyInfo(can_disable=True), + 'interworking.hotspot20': KeyInfo(can_disable=True), + 'interworking.internet': KeyInfo(can_disable=True), + 'interworking.ipv4-availability': KeyInfo(can_disable=True), + 'interworking.ipv6-availability': KeyInfo(can_disable=True), + 'interworking.network-type': KeyInfo(can_disable=True), + 'interworking.operational-classes': KeyInfo(can_disable=True), + 'interworking.operator-names': KeyInfo(can_disable=True), + 'interworking.realms': KeyInfo(can_disable=True), + 'interworking.roaming-ois': KeyInfo(can_disable=True), + 'interworking.uesa': KeyInfo(can_disable=True), + 'interworking.venue-names': KeyInfo(can_disable=True), + 'interworking.venue': KeyInfo(can_disable=True), + 'interworking.wan-at-capacity': KeyInfo(can_disable=True), + 'interworking.wan-downlink-load': KeyInfo(can_disable=True), + 'interworking.wan-downlink': KeyInfo(can_disable=True), + 'interworking.wan-measurement-duration': KeyInfo(can_disable=True), + 'interworking.wan-status': KeyInfo(can_disable=True), + 'interworking.wan-symmetric': KeyInfo(can_disable=True), + 'interworking.wan-uplink-load': KeyInfo(can_disable=True), + 'interworking.wan-uplink': KeyInfo(can_disable=True), + 'interworking': KeyInfo(can_disable=True), + 'l2mtu': KeyInfo(default=1560), + 'mac-address': KeyInfo(), + 'master-interface': KeyInfo(), + 'mtu': KeyInfo(default=1500), + 'name': KeyInfo(), + 'radio-mac': KeyInfo(), + 'security.authentication-types': KeyInfo(can_disable=True), + 'security.connect-group': KeyInfo(can_disable=True), + 'security.connect-priority': KeyInfo(can_disable=True), + 'security.dh-groups': KeyInfo(can_disable=True), + 'security.disable-pmkid': KeyInfo(can_disable=True), + 'security.eap-accounting': KeyInfo(can_disable=True), + 'security.eap-anonymous-identity': KeyInfo(can_disable=True), + 'security.eap-certificate-mode': KeyInfo(can_disable=True), + 'security.eap-methods': KeyInfo(can_disable=True), + 'security.eap-password': KeyInfo(can_disable=True), + 'security.eap-tls-certificate': KeyInfo(can_disable=True), + 'security.eap-username': KeyInfo(can_disable=True), + 'security.encryption': KeyInfo(can_disable=True), + 'security.ft-mobility-domain': KeyInfo(can_disable=True), + 'security.ft-nas-identifier': KeyInfo(can_disable=True), + 'security.ft-over-ds': KeyInfo(can_disable=True), + 'security.ft-preserve-vlanid': KeyInfo(can_disable=True), + 'security.ft-r0-key-lifetime': KeyInfo(can_disable=True), + 'security.ft-reassociation-deadline': KeyInfo(can_disable=True), + 'security.ft': KeyInfo(can_disable=True), + 'security.group-encryption': KeyInfo(can_disable=True), + 'security.group-key-update': KeyInfo(can_disable=True), + 'security.management-encryption': KeyInfo(can_disable=True), + 'security.management-protection': KeyInfo(can_disable=True), + 'security.owe-transition-interface': KeyInfo(can_disable=True), + 'security.passphrase': KeyInfo(can_disable=True), + 'security.sae-anti-clogging-threshold': KeyInfo(can_disable=True), + 'security.sae-max-failure-rate': KeyInfo(can_disable=True), + 'security.sae-pwe': KeyInfo(can_disable=True), + 'security.wps': KeyInfo(can_disable=True), + 'security': KeyInfo(can_disable=True), + 'steering.neighbor-group': KeyInfo(can_disable=True), + 'steering.rrm': KeyInfo(can_disable=True), + 'steering.wnm': KeyInfo(can_disable=True), + 'steering': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'aaa'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'called-format': KeyInfo(can_disable=True), + 'calling-format': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interim-update': KeyInfo(can_disable=True), + 'mac-caching': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'nas-identifier': KeyInfo(can_disable=True), + 'password-format': KeyInfo(can_disable=True), + 'username-format': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'access-list'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(default='accept'), + 'allow-signal-out-of-range': KeyInfo(can_disable=True), + 'client-isolation': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(can_disable=True), + 'mac-address-mask': KeyInfo(can_disable=True), + 'mac-address': KeyInfo(can_disable=True), + 'passphrase': KeyInfo(can_disable=True), + 'radius-accounting': KeyInfo(can_disable=True), + 'signal-range': KeyInfo(can_disable=True), + 'ssid-regexp': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + 'vlan-id': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'cap'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'caps-man-addresses': KeyInfo(default=''), + 'caps-man-certificate-common-names': KeyInfo(default=''), + 'caps-man-names': KeyInfo(default=''), + 'certificate': KeyInfo(default='none'), + 'discovery-interfaces': KeyInfo(default=''), + 'enabled': KeyInfo(default=False), + 'lock-to-caps-man': KeyInfo(default=False), + 'slaves-datapath': KeyInfo(), + 'slaves-static': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wifi', 'capsman'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'ca-certificate': KeyInfo(default=''), + 'certificate': KeyInfo(default='none'), + 'enabled': KeyInfo(default=False), + 'interfaces': KeyInfo(default=''), + 'package-path': KeyInfo(default=''), + 'require-peer-certificate': KeyInfo(default=False), + 'upgrade-policy': KeyInfo(default='none'), + }, + )), + ], + ), + ('interface', 'wifi', 'channel'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'band': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'frequency': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'secondary-frequency': KeyInfo(can_disable=True), + 'skip-dfs-channels': KeyInfo(can_disable=True), + 'width': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'configuration'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'aaa': KeyInfo(can_disable=True), + 'antenna-gain': KeyInfo(can_disable=True), + 'beacon-interval': KeyInfo(can_disable=True), + 'chains': KeyInfo(can_disable=True), + 'channel': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'country': KeyInfo(can_disable=True), + 'datapath': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dtim-period': KeyInfo(can_disable=True), + 'hide-ssid': KeyInfo(default=False), + 'interworking': KeyInfo(can_disable=True), + 'manager': KeyInfo(can_disable=True), + 'mode': KeyInfo(can_disable=True), + 'multicast-enhance': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'qos-classifier': KeyInfo(can_disable=True), + 'security': KeyInfo(can_disable=True), + 'ssid': KeyInfo(can_disable=True), + 'steering': KeyInfo(can_disable=True), + 'tx-chains': KeyInfo(can_disable=True), + 'tx-power': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'datapath'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'bridge-cost': KeyInfo(can_disable=True), + 'bridge-horizon': KeyInfo(can_disable=True), + 'bridge': KeyInfo(can_disable=True), + 'client-isolation': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface-list': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'vlan-id': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'interworking'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + '3gpp-info': KeyInfo(can_disable=True), + 'authentication-types': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connection-capabilities': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'domain-names': KeyInfo(can_disable=True), + 'esr': KeyInfo(can_disable=True), + 'hessid': KeyInfo(can_disable=True), + 'hotspot20-dgaf': KeyInfo(can_disable=True), + 'hotspot20': KeyInfo(can_disable=True), + 'internet': KeyInfo(can_disable=True), + 'ipv4-availability': KeyInfo(can_disable=True), + 'ipv6-availability': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'network-type': KeyInfo(can_disable=True), + 'operational-classes': KeyInfo(can_disable=True), + 'operator-names': KeyInfo(can_disable=True), + 'realms': KeyInfo(can_disable=True), + 'roaming-ois': KeyInfo(can_disable=True), + 'uesa': KeyInfo(can_disable=True), + 'venue-names': KeyInfo(can_disable=True), + 'venue': KeyInfo(can_disable=True), + 'wan-at-capacity': KeyInfo(can_disable=True), + 'wan-downlink-load': KeyInfo(can_disable=True), + 'wan-downlink': KeyInfo(can_disable=True), + 'wan-measurement-duration': KeyInfo(can_disable=True), + 'wan-status': KeyInfo(can_disable=True), + 'wan-symmetric': KeyInfo(can_disable=True), + 'wan-uplink-load': KeyInfo(can_disable=True), + 'wan-uplink': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'provisioning'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(default='none'), + 'address-ranges': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'common-name-regexp': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'identity-regexp': KeyInfo(can_disable=True), + 'master-configuration': KeyInfo(can_disable=True), + 'name-format': KeyInfo(can_disable=True), + 'radio-mac': KeyInfo(can_disable=True), + 'slave-configurations': KeyInfo(can_disable=True), + 'supported-bands': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'security'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'authentication-types': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connect-group': KeyInfo(can_disable=True), + 'connect-priority': KeyInfo(can_disable=True), + 'dh-groups': KeyInfo(can_disable=True), + 'disable-pmkid': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'eap-accounting': KeyInfo(can_disable=True), + 'eap-anonymous-identity': KeyInfo(can_disable=True), + 'eap-certificate-mode': KeyInfo(can_disable=True), + 'eap-methods': KeyInfo(can_disable=True), + 'eap-password': KeyInfo(can_disable=True), + 'eap-tls-certificate': KeyInfo(can_disable=True), + 'eap-username': KeyInfo(can_disable=True), + 'encryption': KeyInfo(can_disable=True), + 'ft-mobility-domain': KeyInfo(can_disable=True), + 'ft-nas-identifier': KeyInfo(can_disable=True), + 'ft-over-ds': KeyInfo(can_disable=True), + 'ft-preserve-vlanid': KeyInfo(can_disable=True), + 'ft-r0-key-lifetime': KeyInfo(can_disable=True), + 'ft-reassociation-deadline': KeyInfo(can_disable=True), + 'ft': KeyInfo(can_disable=True), + 'group-encryption': KeyInfo(can_disable=True), + 'group-key-update': KeyInfo(can_disable=True), + 'management-encryption': KeyInfo(can_disable=True), + 'management-protection': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'owe-transition-interface': KeyInfo(can_disable=True), + 'passphrase': KeyInfo(can_disable=True), + 'sae-anti-clogging-threshold': KeyInfo(can_disable=True), + 'sae-max-failure-rate': KeyInfo(can_disable=True), + 'sae-pwe': KeyInfo(can_disable=True), + 'wps': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifi', 'steering'): APIData( + versioned=[ + ('7.13', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + 'neighbor-group': KeyInfo(can_disable=True), + 'rrm': KeyInfo(can_disable=True), + 'wnm': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifiwave2'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + required_one_of=[['default-name', 'master-interface']], + fields={ + 'aaa': KeyInfo(), + 'arp-timeout': KeyInfo(default='auto'), + 'arp': KeyInfo(default='enabled'), + 'channel': KeyInfo(), + 'configuration': KeyInfo(), + 'datapath': KeyInfo(), + 'default-name': KeyInfo(), + 'disable-running-check': KeyInfo(default=False), + 'interworking': KeyInfo(), + 'l2mtu': KeyInfo(default=1600), + 'mac-address': KeyInfo(), + 'master-interface': KeyInfo(), + 'mtu': KeyInfo(default=1500), + 'name': KeyInfo(), + 'security': KeyInfo(), + 'steering': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'aaa'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'called-format': KeyInfo(can_disable=True), + 'calling-format': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=True), + 'interim-update': KeyInfo(can_disable=True), + 'mac-caching': KeyInfo(can_disable=True), + 'name': KeyInfo(), + 'nas-identifier': KeyInfo(can_disable=True), + 'password-format': KeyInfo(can_disable=True), + 'username-format': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'access-list'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(can_disable=True), + 'allow-signal-out-of-range': KeyInfo(can_disable=True), + 'client-isolation': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=True), + 'interface': KeyInfo(can_disable=True), + 'mac-address': KeyInfo(can_disable=True), + 'mac-address-mask': KeyInfo(can_disable=True), + 'passphrase': KeyInfo(can_disable=True), + 'radius-accounting': KeyInfo(can_disable=True), + 'signal-range': KeyInfo(can_disable=True), + 'ssid-regexp': KeyInfo(), + 'time': KeyInfo(can_disable=True), + 'vlan-id': KeyInfo(can_disable=True), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'cap'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'caps-man-addresses': KeyInfo(default=''), + 'caps-man-certificate-common-names': KeyInfo(default=''), + 'caps-man-names': KeyInfo(default=''), + 'certificate': KeyInfo(default='none'), + 'discovery-interfaces': KeyInfo(default=''), + 'enabled': KeyInfo(default=False), + 'lock-to-caps-man': KeyInfo(default=False), + 'slaves-datapath': KeyInfo(), + 'slaves-static': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'capsman'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'ca-certificate': KeyInfo(default=''), + 'certificate': KeyInfo(default='none'), + 'enabled': KeyInfo(default=False), + 'package-path': KeyInfo(default=''), + 'require-peer-certificate': KeyInfo(default=False), + 'upgrade-policy': KeyInfo(default='none'), + 'interfaces': KeyInfo(default=''), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'channel'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'band': KeyInfo(), + 'frequency': KeyInfo(), + 'name': KeyInfo(), + 'secondary-frequency': KeyInfo(), + 'skip-dfs-channels': KeyInfo(default='disabled'), + 'width': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'configuration'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'aaa': KeyInfo(), + 'antenna-gain': KeyInfo(), + 'beacon-interval': KeyInfo(default=100), + 'chains': KeyInfo(), + 'channel': KeyInfo(), + 'country': KeyInfo(default='United States'), + 'datapath': KeyInfo(), + 'dtim-period': KeyInfo(default=1), + 'hide-ssid': KeyInfo(default=False), + 'interworking': KeyInfo(), + 'manager': KeyInfo(), + 'mode': KeyInfo(default='ap'), + 'name': KeyInfo(), + 'security': KeyInfo(), + 'ssid': KeyInfo(), + 'steering': KeyInfo(), + 'tx-chains': KeyInfo(), + 'tx-power': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'datapath'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'bridge': KeyInfo(), + 'bridge-cost': KeyInfo(), + 'bridge-horizon': KeyInfo(), + 'client-isolation': KeyInfo(default=False), + 'interface-list': KeyInfo(), + 'name': KeyInfo(), + 'openflow-switch': KeyInfo(), + 'vlan-id': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'interworking'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + '3gpp-info': KeyInfo(), + 'authentication-types': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connection-capabilities': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'domain-names': KeyInfo(), + 'esr': KeyInfo(), + 'hessid': KeyInfo(), + 'hotspot20': KeyInfo(), + 'hotspot20-dgaf': KeyInfo(), + 'internet': KeyInfo(), + 'ipv4-availability': KeyInfo(), + 'ipv6-availability': KeyInfo(), + 'name': KeyInfo(), + 'network-type': KeyInfo(), + 'operational-classes': KeyInfo(), + 'operator-names': KeyInfo(), + 'realms': KeyInfo(), + 'roaming-ois': KeyInfo(), + 'uesa': KeyInfo(), + 'venue': KeyInfo(), + 'venue-names': KeyInfo(), + 'wan-at-capacity': KeyInfo(), + 'wan-downlink': KeyInfo(), + 'wan-downlink-load': KeyInfo(), + 'wan-measurement-duration': KeyInfo(), + 'wan-status': KeyInfo(), + 'wan-symmetric': KeyInfo(), + 'wan-uplink': KeyInfo(), + 'wan-uplink-load': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'provisioning'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('action', ), + fields={ + 'action': KeyInfo(default='none'), + 'address-ranges': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'common-name-regexp': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'identity-regexp': KeyInfo(), + 'master-configuration': KeyInfo(), + 'name-format': KeyInfo(), + 'radio-mac': KeyInfo(), + 'slave-configurations': KeyInfo(), + 'supported-bands': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'security'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'authentication-types': KeyInfo(), + 'connect-group': KeyInfo(can_disable=True), + 'connect-priority': KeyInfo(), + 'dh-groups': KeyInfo(), + 'disable-pmkid': KeyInfo(default=False), + 'eap-accounting': KeyInfo(default=False), + 'eap-anonymous-identity': KeyInfo(), + 'eap-certificate-mode': KeyInfo(default='dont-verify-certificate'), + 'eap-methods': KeyInfo(), + 'eap-password': KeyInfo(), + 'eap-tls-certificate': KeyInfo(), + 'eap-username': KeyInfo(), + 'encryption': KeyInfo(default='ccmp'), + 'ft-mobility-domain': KeyInfo(default=0xADC4), + 'ft-nas-identifier': KeyInfo(), + 'ft-over-ds': KeyInfo(default=False), + 'ft-preserve-vlanid': KeyInfo(default=True), + 'ft-r0-key-lifetime': KeyInfo(default='600000s'), + 'ft-reassociation-deadline': KeyInfo(default='20s'), + 'ft': KeyInfo(default=False), + 'group-encryption': KeyInfo(default='ccmp'), + 'group-key-update': KeyInfo(default='24h'), + 'management-encryption': KeyInfo(default='cmac'), + 'management-protection': KeyInfo(), + 'name': KeyInfo(), + 'owe-transition-interface': KeyInfo(), + 'passphrase': KeyInfo(default=''), + 'sae-anti-clogging-threshold': KeyInfo(can_disable=True), + 'sae-max-failure-rate': KeyInfo(can_disable=True), + 'sae-pwe': KeyInfo(default='both'), + 'wps': KeyInfo(default='push-button'), + }, + )), + ], + ), + ('interface', 'wifiwave2', 'steering'): APIData( + versioned=[ + ('7.13', '>=', 'RouterOS 7.13 uses WiFi package'), + ('7.8', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + 'neighbor-group': KeyInfo(), + 'rrm': KeyInfo(), + 'wnm': KeyInfo(), + }, + )), + ], + ), + ('interface', 'wireguard'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'listen-port': KeyInfo(), + 'mtu': KeyInfo(default=1420), + 'name': KeyInfo(), + 'private-key': KeyInfo(), + }, + ), + ), + ('interface', 'wireguard', 'peers'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('public-key', 'interface'), + fields={ + 'allowed-address': KeyInfo(required=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'endpoint-address': KeyInfo(default=''), + 'endpoint-port': KeyInfo(default=0), + 'interface': KeyInfo(), + 'persistent-keepalive': KeyInfo(can_disable=True, remove_value=0), + 'preshared-key': KeyInfo(can_disable=True, remove_value=''), + 'public-key': KeyInfo(), + }, + versioned_fields=[ + ([('7.15', '>=')], 'name', KeyInfo()), + ([('7.15', '>='), ('7.17', '<')], 'is-responder', KeyInfo()), + ([('7.17', '>=')], 'responder', KeyInfo()), + ], + ), + ), + ('interface', 'wireless'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + required_one_of=[['default-name', 'master-interface']], + fields={ + 'adaptive-noise-immunity': KeyInfo(default='none'), + 'allow-sharedkey': KeyInfo(default=False), + 'ampdu-priorities': KeyInfo(default=0), + 'amsdu-limit': KeyInfo(default=8192), + 'amsdu-threshold': KeyInfo(default=8192), + 'antenna-gain': KeyInfo(default=0), + 'antenna-mode': KeyInfo(), + 'area': KeyInfo(default=''), + 'arp': KeyInfo(default='enabled'), + 'arp-timeout': KeyInfo(default='auto'), + 'band': KeyInfo(), + 'basic-rates-a/g': KeyInfo(default='6Mbps'), + 'basic-rates-b': KeyInfo(default='1Mbps'), + 'bridge-mode': KeyInfo(default='enabled'), + 'channel-width': KeyInfo(default='20mhz'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'compression': KeyInfo(default=False), + 'country': KeyInfo(default='etsi'), + 'default-ap-tx-limit': KeyInfo(default=0), + 'default-authentication': KeyInfo(default=True), + 'default-client-tx-limit': KeyInfo(default=0), + 'default-forwarding': KeyInfo(default=True), + 'default-name': KeyInfo(), + 'disable-running-check': KeyInfo(default=False), + 'disabled': KeyInfo(default=True), + 'disconnect-timeout': KeyInfo(default='3s'), + 'distance': KeyInfo(default='dynamic'), + 'frame-lifetime': KeyInfo(default=0), + 'frequency': KeyInfo(), + 'frequency-mode': KeyInfo(default='regulatory-domain'), + 'frequency-offset': KeyInfo(default=0), + 'guard-interval': KeyInfo(default='any'), + 'hide-ssid': KeyInfo(default=False), + 'ht-basic-mcs': KeyInfo(), + 'ht-supported-mcs': KeyInfo(), + 'hw-fragmentation-threshold': KeyInfo(default='disabled'), + 'hw-protection-mode': KeyInfo(default='none'), + 'hw-protection-threshold': KeyInfo(default=0), + 'hw-retries': KeyInfo(default=7), + 'installation': KeyInfo(default='any'), + 'interworking-profile': KeyInfo(default='disabled'), + 'keepalive-frames': KeyInfo(default='enabled'), + 'l2mtu': KeyInfo(default=1600), + 'mac-address': KeyInfo(), + 'master-interface': KeyInfo(), + 'max-station-count': KeyInfo(default=2007), + 'mode': KeyInfo(default='ap-bridge'), + 'mtu': KeyInfo(default=1500), + 'multicast-buffering': KeyInfo(default='enabled'), + 'multicast-helper': KeyInfo(default='default'), + 'name': KeyInfo(), + 'noise-floor-threshold': KeyInfo(default='default'), + 'nv2-cell-radius': KeyInfo(default=30), + 'nv2-downlink-ratio': KeyInfo(default=50), + 'nv2-mode': KeyInfo(default='dynamic-downlink'), + 'nv2-noise-floor-offset': KeyInfo(default='default'), + 'nv2-preshared-key': KeyInfo(default=''), + 'nv2-qos': KeyInfo(default='default'), + 'nv2-queue-count': KeyInfo(default=2), + 'nv2-security': KeyInfo(default='disabled'), + 'nv2-sync-secret': KeyInfo(default=''), + 'on-fail-retry-time': KeyInfo(default='100ms'), + 'preamble-mode': KeyInfo(default='both'), + 'radio-name': KeyInfo(), + 'rate-selection': KeyInfo(default='advanced'), + 'rate-set': KeyInfo(default='default'), + 'running': KeyInfo(read_only=True), + 'rx-chains': KeyInfo(default='0,1'), + 'scan-list': KeyInfo(default='default'), + 'secondary-frequency': KeyInfo(default=''), + 'security-profile': KeyInfo(default='default'), + 'skip-dfs-channels': KeyInfo(default='disabled'), + 'ssid': KeyInfo(required=True), + 'station-bridge-clone-mac': KeyInfo(), + 'station-roaming': KeyInfo(default='disabled'), + 'supported-rates-a/g': KeyInfo(), + 'supported-rates-b': KeyInfo(), + 'tdma-period-size': KeyInfo(default=2), + 'tx-chains': KeyInfo(), + 'tx-power': KeyInfo(default=''), + 'tx-power-mode': KeyInfo(default='default'), + 'update-stats-interval': KeyInfo(default='disabled'), + 'vlan-id': KeyInfo(default=1), + 'vlan-mode': KeyInfo(default='no-tag'), + 'wds-cost-range': KeyInfo(default='50-150'), + 'wds-default-bridge': KeyInfo(default='none'), + 'wds-default-cost': KeyInfo(default=100), + 'wds-ignore-ssid': KeyInfo(default=False), + 'wds-mode': KeyInfo(default='disabled'), + 'wireless-protocol': KeyInfo(default='any'), + 'wmm-support': KeyInfo(default='disabled'), + 'wps-mode': KeyInfo(default='push-button'), + }, + ), + ), + ('interface', 'wireless', 'align'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'active-mode': KeyInfo(default=True), + 'audio-max': KeyInfo(default=-20), + 'audio-min': KeyInfo(default=-100), + 'audio-monitor': KeyInfo(default='00:00:00:00:00:00'), + 'filter-mac': KeyInfo(default='00:00:00:00:00:00'), + 'frame-size': KeyInfo(default=300), + 'frames-per-second': KeyInfo(default=25), + 'receive-all': KeyInfo(default=False), + 'ssid-all': KeyInfo(default=False), + }, + ), + ), + ('interface', 'wireless', 'access-list'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'allow-signal-out-of-range': KeyInfo(default='10s'), + 'ap-tx-limit': KeyInfo(default=0), + 'authentication': KeyInfo(default=True), + 'client-tx-limit': KeyInfo(default=0), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'forwarding': KeyInfo(default=True), + 'interface': KeyInfo(default='any'), + 'mac-address': KeyInfo(default='00:00:00:00:00:00'), + 'management-protection-key': KeyInfo(default=''), + 'private-algo': KeyInfo(default='none'), + 'private-key': KeyInfo(default=''), + 'private-pre-shared-key': KeyInfo(default=''), + 'signal-range': KeyInfo(default='-120..120'), + 'time': KeyInfo(), + 'vlan-id': KeyInfo(default=1), + 'vlan-mode': KeyInfo(default='default'), + }, + ), + ), + ('interface', 'wireless', 'cap'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'bridge': KeyInfo(default='none'), + 'caps-man-addresses': KeyInfo(default=''), + 'caps-man-certificate-common-names': KeyInfo(default=''), + 'caps-man-names': KeyInfo(default=''), + 'certificate': KeyInfo(default='none'), + 'discovery-interfaces': KeyInfo(default=''), + 'enabled': KeyInfo(default=False), + 'interfaces': KeyInfo(default=''), + 'lock-to-caps-man': KeyInfo(default=False), + 'static-virtual': KeyInfo(default=False), + }, + ), + ), + ('interface', 'wireless', 'connect-list'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + '3gpp': KeyInfo(default=''), + 'allow-signal-out-of-range': KeyInfo(default='10s'), + 'area-prefix': KeyInfo(default=''), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connect': KeyInfo(default=True), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(required=True), + 'interworking': KeyInfo(default='any'), + 'iw-asra': KeyInfo(default='any'), + 'iw-authentication-types': KeyInfo(), + 'iw-connection-capabilities': KeyInfo(), + 'iw-esr': KeyInfo(default='any'), + 'iw-hessid': KeyInfo(default='00:00:00:00:00:00'), + 'iw-hotspot20': KeyInfo(default='any'), + 'iw-hotspot20-dgaf': KeyInfo(default='any'), + 'iw-internet': KeyInfo(default='any'), + 'iw-ipv4-availability': KeyInfo(default='any'), + 'iw-ipv6-availability': KeyInfo(default='any'), + 'iw-network-type': KeyInfo(default='wildcard'), + 'iw-realms': KeyInfo(), + 'iw-roaming-ois': KeyInfo(default=''), + 'iw-uesa': KeyInfo(default='any'), + 'iw-venue': KeyInfo(default='any'), + 'mac-address': KeyInfo(default='00:00:00:00:00:00'), + 'security-profile': KeyInfo(default='none'), + 'signal-range': KeyInfo(default='-120..120'), + 'ssid': KeyInfo(default=''), + 'wireless-protocol': KeyInfo(default='any'), + }, + ), + ), + ('interface', 'wireless', 'security-profiles'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'authentication-types': KeyInfo(), + 'disable-pmkid': KeyInfo(default=False), + 'disabled': KeyInfo(default=True), + 'eap-methods': KeyInfo(), + 'group-ciphers': KeyInfo(), + 'group-key-update': KeyInfo(default='5m'), + 'interim-update': KeyInfo(), + 'management-protection': KeyInfo(default='disabled'), + 'management-protection-key': KeyInfo(default=''), + 'mode': KeyInfo(default='none'), + 'mschapv2-password': KeyInfo(default=''), + 'mschapv2-username': KeyInfo(default=''), + 'name': KeyInfo(), + 'radius-called-format': KeyInfo(), + 'radius-eap-accounting': KeyInfo(default=False), + 'radius-mac-accounting': KeyInfo(default=False), + 'radius-mac-authentication': KeyInfo(default=False), + 'radius-mac-caching': KeyInfo(default='disabled'), + 'radius-mac-format': KeyInfo(default='XX:XX:XX:XX:XX:XX'), + 'radius-mac-mode': KeyInfo(default='as-username'), + 'static-algo-0': KeyInfo(default='none'), + 'static-algo-1': KeyInfo(default='none'), + 'static-algo-2': KeyInfo(default='none'), + 'static-algo-3': KeyInfo(default='none'), + 'static-key-0': KeyInfo(), + 'static-key-1': KeyInfo(), + 'static-key-2': KeyInfo(), + 'static-key-3': KeyInfo(), + 'static-sta-private-algo': KeyInfo(default='none'), + 'static-sta-private-key': KeyInfo(), + 'static-transmit-key': KeyInfo(), + 'supplicant-identity': KeyInfo(default='MikroTik'), + 'tls-certificate': KeyInfo(default='none'), + 'tls-mode': KeyInfo(), + 'unicast-ciphers': KeyInfo(), + 'wpa-pre-shared-key': KeyInfo(), + 'wpa2-pre-shared-key': KeyInfo(), + }, + ), + ), + ('interface', 'wireless', 'sniffer'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'channel-time': KeyInfo(default='200ms'), + 'file-limit': KeyInfo(default=10), + 'file-name': KeyInfo(default=''), + 'memory-limit': KeyInfo(default=10), + 'multiple-channels': KeyInfo(default=False), + 'only-headers': KeyInfo(default=False), + 'receive-errors': KeyInfo(default=False), + 'streaming-enabled': KeyInfo(default=False), + 'streaming-max-rate': KeyInfo(default=0), + 'streaming-server': KeyInfo(default='0.0.0.0'), + }, + ), + ), + ('interface', 'wireless', 'snooper'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'channel-time': KeyInfo(default='200ms'), + 'multiple-channels': KeyInfo(default=True), + 'receive-errors': KeyInfo(default=False), + }, + ), + ), + ('iot', 'modbus'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'disabled': KeyInfo(default=True), + 'hardware-port': KeyInfo(default='modbus'), + 'tcp-port': KeyInfo(default=502), + 'timeout': KeyInfo(default=1000), + }, + ), + ), + ('ip', 'accounting'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'account-local-traffic': KeyInfo(default=False), + 'enabled': KeyInfo(default=False), + 'threshold': KeyInfo(default=256), + }, + ), + ), + ('ip', 'accounting', 'web-access'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'accessible-via-web': KeyInfo(default=False), + 'address': KeyInfo(default='0.0.0.0/0'), + }, + ), + ), + ('ip', 'address'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('address', 'interface', ), + fields={ + 'address': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(), + 'network': KeyInfo(automatically_computed_from=('address', )), + }, + ), + ), + ('ip', 'arp'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'address': KeyInfo(default='0.0.0.0'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(required=True), + 'mac-address': KeyInfo(default='00:00:00:00:00:00'), + 'published': KeyInfo(default=False), + }, + ), + ), + ('ip', 'cloud'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + versioned_fields=[ + ([('7.17', '<')], 'ddns-enabled', KeyInfo(default=False)), + ([('7.17', '>=')], 'ddns-enabled', KeyInfo(default='auto')), + ], + fields={ + 'ddns-update-interval': KeyInfo(default='none'), + 'update-time': KeyInfo(default=True), + }, + ), + ), + ('ip', 'cloud', 'advanced'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'use-local-address': KeyInfo(default=False), + }, + ), + ), + ('ip', 'dhcp-client'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('interface', ), + fields={ + 'add-default-route': KeyInfo(default=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'default-route-distance': KeyInfo(default=1), + 'dhcp-options': KeyInfo(default='hostname,clientid', can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(), + 'script': KeyInfo(can_disable=True), + 'use-peer-dns': KeyInfo(default=True), + 'use-peer-ntp': KeyInfo(default=True), + }, + ), + ), + ('ip', 'dhcp-relay'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + fields={ + 'add-relay-info': KeyInfo(default=False), + 'delay-threshold': KeyInfo(can_disable=True, remove_value='none'), + 'dhcp-server': KeyInfo(required=True), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(required=True), + 'local-address': KeyInfo(can_disable=True, remove_value='0.0.0.0'), + 'name': KeyInfo(), + 'relay-info-remote-id': KeyInfo(), + }, + ), + ), + ('ip', 'dhcp-server'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'address-pool': KeyInfo(default='static-only'), + 'allow-dual-stack-queue': KeyInfo(can_disable=True, remove_value=True), + 'always-broadcast': KeyInfo(can_disable=True, remove_value=False), + 'authoritative': KeyInfo(default=True), + 'bootp-lease-time': KeyInfo(default='forever'), + 'bootp-support': KeyInfo(can_disable=True, remove_value='static'), + 'client-mac-limit': KeyInfo(can_disable=True, remove_value='unlimited'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'conflict-detection': KeyInfo(can_disable=True, remove_value=True), + 'delay-threshold': KeyInfo(can_disable=True, remove_value='none'), + 'dhcp-option-set': KeyInfo(can_disable=True, remove_value='none'), + 'disabled': KeyInfo(default=False), + 'insert-queue-before': KeyInfo(can_disable=True, remove_value='first'), + 'interface': KeyInfo(required=True), + 'lease-script': KeyInfo(default=''), + 'lease-time': KeyInfo(default='10m'), + 'name': KeyInfo(), + 'parent-queue': KeyInfo(can_disable=True, remove_value='none'), + 'relay': KeyInfo(can_disable=True, remove_value='0.0.0.0'), + 'server-address': KeyInfo(can_disable=True, remove_value='0.0.0.0'), + 'use-framed-as-classless': KeyInfo(can_disable=True, remove_value=True), + 'use-radius': KeyInfo(default=False), + }, + ), + ), + ('ip', 'dhcp-server', 'config'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'accounting': KeyInfo(default=True), + 'interim-update': KeyInfo(default='0s'), + 'store-leases-disk': KeyInfo(default='5m'), + }, + ), + ), + ('ip', 'dhcp-server', 'lease'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('server', 'address', ), + fields={ + 'address': KeyInfo(), + 'address-lists': KeyInfo(default=''), + 'always-broadcast': KeyInfo(), + 'client-id': KeyInfo(can_disable=True, remove_value=''), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'dhcp-option': KeyInfo(default=''), + 'disabled': KeyInfo(default=False), + 'insert-queue-before': KeyInfo(can_disable=True), + 'mac-address': KeyInfo(can_disable=True, remove_value=''), + 'server': KeyInfo(absent_value='all'), + }, + ), + ), + ('ip', 'dhcp-server', 'network'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('address', ), + fields={ + 'address': KeyInfo(), + 'boot-file-name': KeyInfo(default=''), + 'caps-manager': KeyInfo(default=''), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'dhcp-option': KeyInfo(default=''), + 'dhcp-option-set': KeyInfo(default=''), + 'dns-none': KeyInfo(default=False), + 'dns-server': KeyInfo(default=''), + 'domain': KeyInfo(default=''), + 'gateway': KeyInfo(default=''), + 'netmask': KeyInfo(can_disable=True, remove_value=0), + 'next-server': KeyInfo(can_disable=True), + 'ntp-server': KeyInfo(default=''), + 'wins-server': KeyInfo(default=''), + }, + ), + ), + ('ip', 'dhcp-server', 'option'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + versioned_fields=[ + ([('7.16', '>=')], 'comment', KeyInfo(can_disable=True, remove_value='')), + ], + fields={ + 'code': KeyInfo(required=True), + 'name': KeyInfo(), + 'value': KeyInfo(default=''), + 'force': KeyInfo(default=False), + }, + ), + ), + ('ip', 'dhcp-server', 'option', 'sets'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + versioned_fields=[ + ([('7.16', '>=')], 'comment', KeyInfo(can_disable=True, remove_value='')), + ], + fields={ + 'name': KeyInfo(required=True), + 'options': KeyInfo(), + }, + ), + ), + ('ip', 'dhcp-server', 'matcher'): APIData( + versioned=[ + ('7.4', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + versioned_fields=[ + ([('7.16', '>=')], 'comment', KeyInfo(can_disable=True, remove_value='')), + ([('7.16', '>=')], 'matching-type', KeyInfo()), + ], + fields={ + 'address-pool': KeyInfo(default='none'), + 'code': KeyInfo(required=True), + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(required=True), + 'option-set': KeyInfo(), + 'server': KeyInfo(default='all'), + 'value': KeyInfo(required=True), + }, + )), + ], + ), + ('ip', 'dns'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + versioned_fields=[ + ([('7.8', '>=')], 'doh-max-concurrent-queries', KeyInfo(default=50)), + ([('7.8', '>=')], 'doh-max-server-connections', KeyInfo(default=5)), + ([('7.8', '>=')], 'doh-timeout', KeyInfo(default='5s')), + ([('7.16', '>=')], 'mdns-repeat-ifaces', KeyInfo()), + ], + fields={ + 'allow-remote-requests': KeyInfo(), + 'cache-max-ttl': KeyInfo(default='1w'), + 'cache-size': KeyInfo(default='2048KiB'), + 'max-concurrent-queries': KeyInfo(default=100), + 'max-concurrent-tcp-sessions': KeyInfo(default=20), + 'max-udp-packet-size': KeyInfo(default=4096), + 'query-server-timeout': KeyInfo(default='2s'), + 'query-total-timeout': KeyInfo(default='10s'), + 'servers': KeyInfo(default=''), + 'use-doh-server': KeyInfo(default=''), + 'verify-doh-cert': KeyInfo(default=False), + }, + ), + ), + ('ip', 'dns', 'adlist'): APIData( + versioned=[ + ('7.15', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'file': KeyInfo(default=''), + 'match-count': KeyInfo(read_only=True), + 'name-count': KeyInfo(read_only=True), + 'ssl-verify': KeyInfo(default=True), + 'url': KeyInfo(default=''), + }, + )), + ], + ), + ('ip', 'dns', 'forwarders'): APIData( + versioned=[ + ('7.17', '>=', VersionedAPIData( + fully_understood=True, + required_one_of=[['dns-servers', 'doh-servers']], + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'dns-servers': KeyInfo(default=''), + 'doh-servers': KeyInfo(default=''), + 'name': KeyInfo(required=True), + 'verify-doh-cert': KeyInfo(default=True), + }, + )), + ], + ), + ('ip', 'dns', 'static'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + required_one_of=[['name', 'regexp']], + mutually_exclusive=[['name', 'regexp']], + versioned_fields=[ + ([('7.5', '>=')], 'address-list', KeyInfo()), + ([('7.5', '>=')], 'match-subdomain', KeyInfo(default=False)), + ], + fields={ + 'address': KeyInfo(), + 'cname': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'forward-to': KeyInfo(), + 'mx-exchange': KeyInfo(), + 'mx-preference': KeyInfo(), + 'name': KeyInfo(), + 'ns': KeyInfo(), + 'regexp': KeyInfo(), + 'srv-port': KeyInfo(), + 'srv-priority': KeyInfo(), + 'srv-target': KeyInfo(), + 'srv-weight': KeyInfo(), + 'text': KeyInfo(), + 'ttl': KeyInfo(default='1d'), + 'type': KeyInfo(), + }, + ), + ), + ('ip', 'firewall', 'address-list'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('address', 'list', ), + fields={ + 'address': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'list': KeyInfo(), + }, + ), + ), + ('ip', 'firewall', 'filter'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + stratify_keys=('chain', ), + fields={ + 'action': KeyInfo(), + 'address-list': KeyInfo(can_disable=True), + 'address-list-timeout': KeyInfo(can_disable=True), + 'chain': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connection-bytes': KeyInfo(can_disable=True), + 'connection-limit': KeyInfo(can_disable=True), + 'connection-mark': KeyInfo(can_disable=True), + 'connection-nat-state': KeyInfo(can_disable=True), + 'connection-rate': KeyInfo(can_disable=True), + 'connection-state': KeyInfo(can_disable=True), + 'connection-type': KeyInfo(can_disable=True), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(can_disable=True), + 'dst-address': KeyInfo(can_disable=True), + 'dst-address-list': KeyInfo(can_disable=True), + 'dst-address-type': KeyInfo(can_disable=True), + 'dst-limit': KeyInfo(can_disable=True), + 'dst-port': KeyInfo(can_disable=True), + 'fragment': KeyInfo(can_disable=True), + 'hotspot': KeyInfo(can_disable=True), + 'hw-offload': KeyInfo(can_disable=True), + 'icmp-options': KeyInfo(can_disable=True), + 'in-bridge-port': KeyInfo(can_disable=True), + 'in-bridge-port-list': KeyInfo(can_disable=True), + 'in-interface': KeyInfo(can_disable=True), + 'in-interface-list': KeyInfo(can_disable=True), + 'ingress-priority': KeyInfo(can_disable=True), + 'ipsec-policy': KeyInfo(can_disable=True), + 'ipv4-options': KeyInfo(can_disable=True), + 'jump-target': KeyInfo(can_disable=True), + 'layer7-protocol': KeyInfo(can_disable=True), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(default=False), + 'log-prefix': KeyInfo(default=''), + 'nth': KeyInfo(can_disable=True), + 'out-bridge-port': KeyInfo(can_disable=True), + 'out-bridge-port-list': KeyInfo(can_disable=True), + 'out-interface': KeyInfo(can_disable=True), + 'out-interface-list': KeyInfo(can_disable=True), + 'p2p': KeyInfo(can_disable=True), + 'packet-mark': KeyInfo(can_disable=True), + 'packet-size': KeyInfo(can_disable=True), + 'per-connection-classifier': KeyInfo(can_disable=True), + 'port': KeyInfo(can_disable=True), + 'priority': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'psd': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'realm': KeyInfo(can_disable=True), + 'reject-with': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'routing-table': KeyInfo(can_disable=True), + 'src-address': KeyInfo(can_disable=True), + 'src-address-list': KeyInfo(can_disable=True), + 'src-address-type': KeyInfo(can_disable=True), + 'src-mac-address': KeyInfo(can_disable=True), + 'src-port': KeyInfo(can_disable=True), + 'tcp-flags': KeyInfo(can_disable=True), + 'tcp-mss': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + 'tls-host': KeyInfo(can_disable=True), + 'ttl': KeyInfo(can_disable=True), + }, + ), + ), + ('ip', 'firewall', 'mangle'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + stratify_keys=('chain', ), + versioned_fields=[ + ([('7.19', '<')], 'passthrough', KeyInfo(can_disable=True)), + ([('7.19', '>=')], 'passthrough', KeyInfo(default=True)), + ], + fields={ + 'action': KeyInfo(), + 'address-list': KeyInfo(can_disable=True), + 'address-list-timeout': KeyInfo(can_disable=True), + 'chain': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connection-bytes': KeyInfo(can_disable=True), + 'connection-limit': KeyInfo(can_disable=True), + 'connection-mark': KeyInfo(can_disable=True), + 'connection-nat-state': KeyInfo(can_disable=True), + 'connection-rate': KeyInfo(can_disable=True), + 'connection-state': KeyInfo(can_disable=True), + 'connection-type': KeyInfo(can_disable=True), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(can_disable=True), + 'dst-address': KeyInfo(can_disable=True), + 'dst-address-list': KeyInfo(can_disable=True), + 'dst-address-type': KeyInfo(can_disable=True), + 'dst-limit': KeyInfo(can_disable=True), + 'dst-port': KeyInfo(can_disable=True), + 'fragment': KeyInfo(can_disable=True), + 'hotspot': KeyInfo(can_disable=True), + 'icmp-options': KeyInfo(can_disable=True), + 'in-bridge-port': KeyInfo(can_disable=True), + 'in-bridge-port-list': KeyInfo(can_disable=True), + 'in-interface': KeyInfo(can_disable=True), + 'in-interface-list': KeyInfo(can_disable=True), + 'ingress-priority': KeyInfo(can_disable=True), + 'ipsec-policy': KeyInfo(can_disable=True), + 'ipv4-options': KeyInfo(can_disable=True), + 'jump-target': KeyInfo(can_disable=True), + 'layer7-protocol': KeyInfo(can_disable=True), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(default=False), + 'log-prefix': KeyInfo(default=''), + 'new-connection-mark': KeyInfo(can_disable=True), + 'new-dscp': KeyInfo(can_disable=True), + 'new-mss': KeyInfo(can_disable=True), + 'new-packet-mark': KeyInfo(can_disable=True), + 'new-priority': KeyInfo(can_disable=True), + 'new-routing-mark': KeyInfo(can_disable=True), + 'new-ttl': KeyInfo(can_disable=True), + 'nth': KeyInfo(can_disable=True), + 'out-bridge-port': KeyInfo(can_disable=True), + 'out-bridge-port-list': KeyInfo(can_disable=True), + 'out-interface': KeyInfo(can_disable=True), + 'out-interface-list': KeyInfo(can_disable=True), + 'p2p': KeyInfo(can_disable=True), + 'packet-mark': KeyInfo(can_disable=True), + 'packet-size': KeyInfo(can_disable=True), + 'per-connection-classifier': KeyInfo(can_disable=True), + 'port': KeyInfo(can_disable=True), + 'priority': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'psd': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'realm': KeyInfo(can_disable=True), + 'route-dst': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'routing-table': KeyInfo(can_disable=True), + 'sniff-id': KeyInfo(can_disable=True), + 'sniff-target': KeyInfo(can_disable=True), + 'sniff-target-port': KeyInfo(can_disable=True), + 'src-address': KeyInfo(can_disable=True), + 'src-address-list': KeyInfo(can_disable=True), + 'src-address-type': KeyInfo(can_disable=True), + 'src-mac-address': KeyInfo(can_disable=True), + 'src-port': KeyInfo(can_disable=True), + 'tcp-flags': KeyInfo(can_disable=True), + 'tcp-mss': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + 'tls-host': KeyInfo(can_disable=True), + 'ttl': KeyInfo(can_disable=True), + }, + ), + ), + ('ip', 'firewall', 'nat'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + stratify_keys=('chain', ), + fields={ + 'action': KeyInfo(), + 'address-list': KeyInfo(can_disable=True), + 'address-list-timeout': KeyInfo(can_disable=True), + 'chain': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connection-bytes': KeyInfo(can_disable=True), + 'connection-limit': KeyInfo(can_disable=True), + 'connection-mark': KeyInfo(can_disable=True), + 'connection-rate': KeyInfo(can_disable=True), + 'connection-type': KeyInfo(can_disable=True), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(can_disable=True), + 'dst-address': KeyInfo(can_disable=True), + 'dst-address-list': KeyInfo(can_disable=True), + 'dst-address-type': KeyInfo(can_disable=True), + 'dst-limit': KeyInfo(can_disable=True), + 'dst-port': KeyInfo(can_disable=True), + 'fragment': KeyInfo(can_disable=True), + 'hotspot': KeyInfo(can_disable=True), + 'icmp-options': KeyInfo(can_disable=True), + 'in-bridge-port': KeyInfo(can_disable=True), + 'in-bridge-port-list': KeyInfo(can_disable=True), + 'in-interface': KeyInfo(can_disable=True), + 'in-interface-list': KeyInfo(can_disable=True), + 'ingress-priority': KeyInfo(can_disable=True), + 'ipsec-policy': KeyInfo(can_disable=True), + 'ipv4-options': KeyInfo(can_disable=True), + 'jump-target': KeyInfo(can_disable=True), + 'layer7-protocol': KeyInfo(can_disable=True), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(default=False), + 'log-prefix': KeyInfo(default=''), + 'nth': KeyInfo(can_disable=True), + 'out-bridge-port': KeyInfo(can_disable=True), + 'out-bridge-port-list': KeyInfo(can_disable=True), + 'out-interface': KeyInfo(can_disable=True), + 'out-interface-list': KeyInfo(can_disable=True), + 'packet-mark': KeyInfo(can_disable=True), + 'packet-size': KeyInfo(can_disable=True), + 'per-connection-classifier': KeyInfo(can_disable=True), + 'port': KeyInfo(can_disable=True), + 'priority': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'psd': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'randomise-ports': KeyInfo(can_disable=True), + 'realm': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'same-not-by-dst': KeyInfo(can_disable=True), + 'src-address': KeyInfo(can_disable=True), + 'src-address-list': KeyInfo(can_disable=True), + 'src-address-type': KeyInfo(can_disable=True), + 'src-mac-address': KeyInfo(can_disable=True), + 'src-port': KeyInfo(can_disable=True), + 'tcp-mss': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + 'tls-host': KeyInfo(can_disable=True), + 'to-addresses': KeyInfo(can_disable=True), + 'to-ports': KeyInfo(can_disable=True), + 'ttl': KeyInfo(can_disable=True), + }, + ), + ), + ('ip', 'firewall', 'raw'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + stratify_keys=('chain',), + fields={ + 'action': KeyInfo(), + 'address-list': KeyInfo(can_disable=True), + 'address-list-timeout': KeyInfo(can_disable=True), + 'chain': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(can_disable=True), + 'dst-address': KeyInfo(can_disable=True), + 'dst-address-list': KeyInfo(can_disable=True), + 'dst-address-type': KeyInfo(can_disable=True), + 'dst-limit': KeyInfo(can_disable=True), + 'dst-port': KeyInfo(can_disable=True), + 'fragment': KeyInfo(can_disable=True), + 'hotspot': KeyInfo(can_disable=True), + 'icmp-options': KeyInfo(can_disable=True), + 'in-bridge-port': KeyInfo(can_disable=True), + 'in-bridge-port-list': KeyInfo(can_disable=True), + 'in-interface': KeyInfo(can_disable=True), + 'in-interface-list': KeyInfo(can_disable=True), + 'ingress-priority': KeyInfo(can_disable=True), + 'ipsec-policy': KeyInfo(can_disable=True), + 'ipv4-options': KeyInfo(can_disable=True), + 'jump-target': KeyInfo(can_disable=True), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(default=False), + 'log-prefix': KeyInfo(default=''), + 'nth': KeyInfo(can_disable=True), + 'out-bridge-port': KeyInfo(can_disable=True), + 'out-bridge-port-list': KeyInfo(can_disable=True), + 'out-interface': KeyInfo(can_disable=True), + 'out-interface-list': KeyInfo(can_disable=True), + 'packet-mark': KeyInfo(can_disable=True), + 'packet-size': KeyInfo(can_disable=True), + 'per-connection-classifier': KeyInfo(can_disable=True), + 'port': KeyInfo(can_disable=True), + 'priority': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'psd': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'src-address': KeyInfo(can_disable=True), + 'src-address-list': KeyInfo(can_disable=True), + 'src-address-type': KeyInfo(can_disable=True), + 'src-mac-address': KeyInfo(can_disable=True), + 'src-port': KeyInfo(can_disable=True), + 'tcp-flags': KeyInfo(can_disable=True), + 'tcp-mss': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + 'tls-host': KeyInfo(can_disable=True), + 'ttl': KeyInfo(can_disable=True), + }, + ), + ), + ('ip', 'hotspot', 'user'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(), + 'name': KeyInfo(), + }, + ), + ), + ('ip', 'ipsec', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'accounting': KeyInfo(default=True), + 'interim-update': KeyInfo(default='0s'), + 'xauth-use-radius': KeyInfo(default=False), + }, + ), + ), + ('ip', 'proxy'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'always-from-cache': KeyInfo(default=False), + 'anonymous': KeyInfo(default=False), + 'cache-administrator': KeyInfo(default='webmaster'), + 'cache-hit-dscp': KeyInfo(default=4), + 'cache-on-disk': KeyInfo(default=False), + 'cache-path': KeyInfo(default='web-proxy'), + 'enabled': KeyInfo(default=False), + 'max-cache-object-size': KeyInfo(default='2048KiB'), + 'max-cache-size': KeyInfo(default='unlimited'), + 'max-client-connections': KeyInfo(default=600), + 'max-fresh-time': KeyInfo(default='3d'), + 'max-server-connections': KeyInfo(default=600), + 'parent-proxy': KeyInfo(default='::'), + 'parent-proxy-port': KeyInfo(default=0), + 'port': KeyInfo(default=8080), + 'serialize-connections': KeyInfo(default=False), + 'src-address': KeyInfo(default='::'), + }, + ), + ), + ('ip', 'smb'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allow-guests': KeyInfo(default=True), + 'comment': KeyInfo(default='MikrotikSMB'), + 'domain': KeyInfo(default='MSHOME'), + 'enabled': KeyInfo(default=False), + 'interfaces': KeyInfo(default='all'), + }, + ), + ), + ('ip', 'smb', 'shares'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'directory': KeyInfo(), + 'disabled': KeyInfo(), + 'max-sessions': KeyInfo(), + 'name': KeyInfo(), + }, + ), + ), + ('ip', 'smb', 'users'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'disabled': KeyInfo(), + 'name': KeyInfo(), + 'password': KeyInfo(), + 'read-only': KeyInfo(), + }, + ), + ), + ('ip', 'socks'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'auth-method': KeyInfo(default='none'), + 'connection-idle-timeout': KeyInfo(default='2m'), + 'enabled': KeyInfo(default=False), + 'max-connections': KeyInfo(default=200), + 'port': KeyInfo(default=1080), + 'version': KeyInfo(default=4), + }, + ), + ), + ('ip', 'ssh'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + versioned_fields=[ + ([('7.9', '>=')], 'host-key-type', KeyInfo(default='rsa')), + ], + fields={ + 'allow-none-crypto': KeyInfo(default=False), + 'always-allow-password-login': KeyInfo(default=False), + 'forwarding-enabled': KeyInfo(default=False), + 'host-key-size': KeyInfo(default=2048), + 'strong-crypto': KeyInfo(default=False), + }, + ), + ), + ('ip', 'tftp', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'max-block-size': KeyInfo(default=4096), + }, + ), + ), + ('ip', 'traffic-flow'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'active-flow-timeout': KeyInfo(default='30m'), + 'cache-entries': KeyInfo(default='32k'), + 'enabled': KeyInfo(default=False), + 'inactive-flow-timeout': KeyInfo(default='15s'), + 'interfaces': KeyInfo(default='all'), + 'packet-sampling': KeyInfo(default=False), + 'sampling-interval': KeyInfo(default=0), + 'sampling-space': KeyInfo(default=0), + }, + ), + ), + ('ip', 'traffic-flow', 'ipfix'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'bytes': KeyInfo(default=True), + 'dst-address': KeyInfo(default=True), + 'dst-address-mask': KeyInfo(default=True), + 'dst-mac-address': KeyInfo(default=True), + 'dst-port': KeyInfo(default=True), + 'first-forwarded': KeyInfo(default=True), + 'gateway': KeyInfo(default=True), + 'icmp-code': KeyInfo(default=True), + 'icmp-type': KeyInfo(default=True), + 'igmp-type': KeyInfo(default=True), + 'in-interface': KeyInfo(default=True), + 'ip-header-length': KeyInfo(default=True), + 'ip-total-length': KeyInfo(default=True), + 'ipv6-flow-label': KeyInfo(default=True), + 'is-multicast': KeyInfo(default=True), + 'last-forwarded': KeyInfo(default=True), + 'nat-dst-address': KeyInfo(default=True), + 'nat-dst-port': KeyInfo(default=True), + 'nat-events': KeyInfo(default=False), + 'nat-src-address': KeyInfo(default=True), + 'nat-src-port': KeyInfo(default=True), + 'out-interface': KeyInfo(default=True), + 'packets': KeyInfo(default=True), + 'protocol': KeyInfo(default=True), + 'src-address': KeyInfo(default=True), + 'src-address-mask': KeyInfo(default=True), + 'src-mac-address': KeyInfo(default=True), + 'src-port': KeyInfo(default=True), + 'sys-init-time': KeyInfo(default=True), + 'tcp-ack-num': KeyInfo(default=True), + 'tcp-flags': KeyInfo(default=True), + 'tcp-seq-num': KeyInfo(default=True), + 'tcp-window-size': KeyInfo(default=True), + 'tos': KeyInfo(default=True), + 'ttl': KeyInfo(default=True), + 'udp-length': KeyInfo(default=True), + }, + ), + ), + ('ip', 'traffic-flow', 'target'): APIData( + unversioned=VersionedAPIData( + single_value=False, + fully_understood=True, + fields={ + 'address': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'dst-address': KeyInfo(), + 'port': KeyInfo(default=2055), + 'src-address': KeyInfo(), + 'v9-template-refresh': KeyInfo(default=20), + 'v9-template-timeout': KeyInfo(), + 'version': KeyInfo(), + }, + ), + ), + ('ip', 'upnp'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allow-disable-external-interface': KeyInfo(default=False), + 'enabled': KeyInfo(default=False), + 'show-dummy-rule': KeyInfo(default=True), + }, + ), + ), + ('ip', 'upnp', 'interfaces'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('interface', 'type'), + fields={ + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(), + 'type': KeyInfo(), + 'forced-ip': KeyInfo(can_disable=True), + }, + ), + ), + ('ipv6', 'dhcp-client'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('interface', 'request'), + fields={ + 'add-default-route': KeyInfo(default=False), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'default-route-distance': KeyInfo(default=1), + 'dhcp-options': KeyInfo(default=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(), + 'pool-name': KeyInfo(required=True), + 'pool-prefix-length': KeyInfo(default=64), + 'prefix-hint': KeyInfo(default='::/0'), + 'request': KeyInfo(), + 'use-peer-dns': KeyInfo(default=True), + }, + versioned_fields=[ + # Mikrotik does not provide exact version in official changelogs. + # The 7.15 version is the earliest, found option in router config backups: + ([('7.15', '>=')], 'script', KeyInfo(default='')), + ([('7.15', '>=')], 'custom-duid', KeyInfo(default='')), + ([('7.15', '>=')], 'use-interface-duid', KeyInfo(default=False)), + ([('7.15', '>=')], 'validate-server-duid', KeyInfo(default=True)), + ], + ), + ), + ('ipv6', 'dhcp-server'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'address-pool': KeyInfo(required=True), + 'allow-dual-stack-queue': KeyInfo(can_disable=True, remove_value=True), + 'binding-script': KeyInfo(can_disable=True, remove_value=''), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'dhcp-option': KeyInfo(default=''), + 'disabled': KeyInfo(default=False), + 'insert-queue-before': KeyInfo(can_disable=True, remove_value='first'), + 'interface': KeyInfo(required=True), + 'lease-time': KeyInfo(default='3d'), + 'name': KeyInfo(), + 'parent-queue': KeyInfo(can_disable=True, remove_value='none'), + 'preference': KeyInfo(default=255), + 'rapid-commit': KeyInfo(default=True), + 'route-distance': KeyInfo(default=1), + 'use-radius': KeyInfo(default=False), + }, + ), + ), + ('ipv6', 'dhcp-server', 'option'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + fields={ + 'code': KeyInfo(required=True), + 'name': KeyInfo(), + 'value': KeyInfo(default=''), + }, + ), + ), + ('ipv6', 'firewall', 'address-list'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('address', 'list', ), + fields={ + 'address': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'list': KeyInfo(), + }, + ), + ), + ('ipv6', 'firewall', 'filter'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + stratify_keys=('chain', ), + fields={ + 'action': KeyInfo(), + 'address-list': KeyInfo(can_disable=True), + 'address-list-timeout': KeyInfo(can_disable=True), + 'chain': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connection-bytes': KeyInfo(can_disable=True), + 'connection-limit': KeyInfo(can_disable=True), + 'connection-mark': KeyInfo(can_disable=True), + 'connection-rate': KeyInfo(can_disable=True), + 'connection-state': KeyInfo(can_disable=True), + 'connection-type': KeyInfo(can_disable=True), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(can_disable=True), + 'dst-address': KeyInfo(can_disable=True), + 'dst-address-list': KeyInfo(can_disable=True), + 'dst-address-type': KeyInfo(can_disable=True), + 'dst-limit': KeyInfo(can_disable=True), + 'dst-port': KeyInfo(can_disable=True), + 'headers': KeyInfo(can_disable=True), + 'hop-limit': KeyInfo(can_disable=True), + 'icmp-options': KeyInfo(can_disable=True), + 'in-bridge-port': KeyInfo(can_disable=True), + 'in-bridge-port-list': KeyInfo(can_disable=True), + 'in-interface': KeyInfo(can_disable=True), + 'in-interface-list': KeyInfo(can_disable=True), + 'ingress-priority': KeyInfo(can_disable=True), + 'ipsec-policy': KeyInfo(can_disable=True), + 'jump-target': KeyInfo(can_disable=True), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(can_disable=False), + 'log-prefix': KeyInfo(can_disable=False), + 'nth': KeyInfo(can_disable=True), + 'out-bridge-port': KeyInfo(can_disable=True), + 'out-bridge-port-list': KeyInfo(can_disable=True), + 'out-interface': KeyInfo(can_disable=True), + 'out-interface-list': KeyInfo(can_disable=True), + 'packet-mark': KeyInfo(can_disable=True), + 'packet-size': KeyInfo(can_disable=True), + 'per-connection-classifier': KeyInfo(can_disable=True), + 'port': KeyInfo(can_disable=True), + 'priority': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'reject-with': KeyInfo(), + 'src-address': KeyInfo(can_disable=True), + 'src-address-list': KeyInfo(can_disable=True), + 'src-address-type': KeyInfo(can_disable=True), + 'src-mac-address': KeyInfo(can_disable=True), + 'src-port': KeyInfo(can_disable=True), + 'tcp-flags': KeyInfo(can_disable=True), + 'tcp-mss': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + }, + ), + ), + ('ipv6', 'firewall', 'mangle'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + stratify_keys=('chain', ), + fields={ + 'action': KeyInfo(), + 'address-list': KeyInfo(), + 'address-list-timeout': KeyInfo(), + 'chain': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connection-bytes': KeyInfo(can_disable=True), + 'connection-limit': KeyInfo(can_disable=True), + 'connection-mark': KeyInfo(can_disable=True), + 'connection-rate': KeyInfo(can_disable=True), + 'connection-state': KeyInfo(can_disable=True), + 'connection-type': KeyInfo(can_disable=True), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(can_disable=True), + 'dst-address': KeyInfo(can_disable=True), + 'dst-address-list': KeyInfo(can_disable=True), + 'dst-address-type': KeyInfo(can_disable=True), + 'dst-limit': KeyInfo(can_disable=True), + 'dst-port': KeyInfo(can_disable=True), + 'dst-prefix': KeyInfo(), + 'headers': KeyInfo(can_disable=True), + 'hop-limit': KeyInfo(can_disable=True), + 'icmp-options': KeyInfo(can_disable=True), + 'in-bridge-port': KeyInfo(can_disable=True), + 'in-bridge-port-list': KeyInfo(can_disable=True), + 'in-interface': KeyInfo(can_disable=True), + 'in-interface-list': KeyInfo(can_disable=True), + 'ingress-priority': KeyInfo(can_disable=True), + 'ipsec-policy': KeyInfo(can_disable=True), + 'jump-target': KeyInfo(), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(), + 'log-prefix': KeyInfo(), + 'new-connection-mark': KeyInfo(), + 'new-dscp': KeyInfo(), + 'new-hop-limit': KeyInfo(), + 'new-mss': KeyInfo(), + 'new-packet-mark': KeyInfo(), + 'new-routing-mark': KeyInfo(), + 'nth': KeyInfo(can_disable=True), + 'out-bridge-port': KeyInfo(can_disable=True), + 'out-bridge-port-list': KeyInfo(can_disable=True), + 'out-interface': KeyInfo(can_disable=True), + 'out-interface-list': KeyInfo(can_disable=True), + 'packet-mark': KeyInfo(can_disable=True), + 'packet-size': KeyInfo(can_disable=True), + 'passthrough': KeyInfo(), + 'per-connection-classifier': KeyInfo(can_disable=True), + 'port': KeyInfo(can_disable=True), + 'priority': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'sniff-id': KeyInfo(), + 'sniff-target': KeyInfo(), + 'sniff-target-port': KeyInfo(), + 'src-address': KeyInfo(can_disable=True), + 'src-address-list': KeyInfo(can_disable=True), + 'src-address-type': KeyInfo(can_disable=True), + 'src-mac-address': KeyInfo(can_disable=True), + 'src-port': KeyInfo(can_disable=True), + 'src-prefix': KeyInfo(), + 'tcp-flags': KeyInfo(can_disable=True), + 'tcp-mss': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + 'tls-host': KeyInfo(can_disable=True), + } + ), + ), + ('ipv6', 'firewall', 'nat'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + stratify_keys=('chain', ), + fields={ + 'action': KeyInfo(), + 'address-list': KeyInfo(), + 'address-list-timeout': KeyInfo(), + 'chain': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'connection-bytes': KeyInfo(can_disable=True), + 'connection-limit': KeyInfo(can_disable=True), + 'connection-mark': KeyInfo(can_disable=True), + 'connection-rate': KeyInfo(can_disable=True), + 'connection-state': KeyInfo(can_disable=True), + 'connection-type': KeyInfo(can_disable=True), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(can_disable=True), + 'dst-address': KeyInfo(can_disable=True), + 'dst-address-list': KeyInfo(can_disable=True), + 'dst-address-type': KeyInfo(can_disable=True), + 'dst-limit': KeyInfo(can_disable=True), + 'dst-port': KeyInfo(can_disable=True), + 'icmp-options': KeyInfo(can_disable=True), + 'in-bridge-port': KeyInfo(can_disable=True), + 'in-bridge-port-list': KeyInfo(can_disable=True), + 'in-interface': KeyInfo(can_disable=True), + 'in-interface-list': KeyInfo(can_disable=True), + 'ingress-priority': KeyInfo(can_disable=True), + 'ipsec-policy': KeyInfo(can_disable=True), + 'jump-target': KeyInfo(), + 'layer7-protocol': KeyInfo(can_disable=True), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(), + 'log-prefix': KeyInfo(), + 'out-bridge-port': KeyInfo(can_disable=True), + 'out-bridge-port-list': KeyInfo(can_disable=True), + 'out-interface': KeyInfo(can_disable=True), + 'out-interface-list': KeyInfo(can_disable=True), + 'packet-mark': KeyInfo(can_disable=True), + 'packet-size': KeyInfo(can_disable=True), + 'per-connection-classifier': KeyInfo(can_disable=True), + 'port': KeyInfo(can_disable=True), + 'priority': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'src-address': KeyInfo(can_disable=True), + 'src-address-list': KeyInfo(can_disable=True), + 'src-address-type': KeyInfo(can_disable=True), + 'src-mac-address': KeyInfo(can_disable=True), + 'src-port': KeyInfo(can_disable=True), + 'tcp-flags': KeyInfo(can_disable=True), + 'tcp-mss': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + 'tls-host': KeyInfo(can_disable=True), + 'to-addresses': KeyInfo(can_disable=True), + 'to-ports': KeyInfo(can_disable=True), + }, + ), + ), + ('ipv6', 'firewall', 'raw'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + stratify_keys=('chain',), + fields={ + 'action': KeyInfo(), + 'address-list': KeyInfo(), + 'address-list-timeout': KeyInfo(), + 'chain': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'dscp': KeyInfo(can_disable=True), + 'dst-address': KeyInfo(can_disable=True), + 'dst-address-list': KeyInfo(can_disable=True), + 'dst-address-type': KeyInfo(can_disable=True), + 'dst-limit': KeyInfo(can_disable=True), + 'dst-port': KeyInfo(can_disable=True), + 'headers': KeyInfo(can_disable=True), + 'hop-limit': KeyInfo(can_disable=True), + 'icmp-options': KeyInfo(can_disable=True), + 'in-bridge-port': KeyInfo(can_disable=True), + 'in-bridge-port-list': KeyInfo(can_disable=True), + 'in-interface': KeyInfo(can_disable=True), + 'in-interface-list': KeyInfo(can_disable=True), + 'ingress-priority': KeyInfo(can_disable=True), + 'ipsec-policy': KeyInfo(can_disable=True), + 'jump-target': KeyInfo(), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(), + 'log-prefix': KeyInfo(), + 'nth': KeyInfo(can_disable=True), + 'out-bridge-port': KeyInfo(can_disable=True), + 'out-bridge-port-list': KeyInfo(can_disable=True), + 'out-interface': KeyInfo(can_disable=True), + 'out-interface-list': KeyInfo(can_disable=True), + 'packet-mark': KeyInfo(can_disable=True), + 'packet-size': KeyInfo(can_disable=True), + 'per-connection-classifier': KeyInfo(can_disable=True), + 'port': KeyInfo(can_disable=True), + 'priority': KeyInfo(can_disable=True), + 'protocol': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'src-address': KeyInfo(can_disable=True), + 'src-address-list': KeyInfo(can_disable=True), + 'src-address-type': KeyInfo(can_disable=True), + 'src-mac-address': KeyInfo(can_disable=True), + 'src-port': KeyInfo(can_disable=True), + 'tcp-flags': KeyInfo(can_disable=True), + 'tcp-mss': KeyInfo(can_disable=True), + 'time': KeyInfo(can_disable=True), + 'tls-host': KeyInfo(can_disable=True), + } + ), + ), + ('ipv6', 'nd'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('interface', ), + fields={ + 'advertise-dns': KeyInfo(default=True), + 'advertise-mac-address': KeyInfo(default=True), + 'disabled': KeyInfo(default=False), + 'dns': KeyInfo(default=''), + 'hop-limit': KeyInfo(default='unspecified'), + 'interface': KeyInfo(), + 'managed-address-configuration': KeyInfo(default=False), + 'mtu': KeyInfo(default='unspecified'), + 'other-configuration': KeyInfo(default=False), + 'ra-delay': KeyInfo(default='3s'), + 'ra-interval': KeyInfo(default='3m20s-10m'), + 'ra-lifetime': KeyInfo(default='30m'), + 'ra-preference': KeyInfo(default='medium'), + 'reachable-time': KeyInfo(default='unspecified'), + 'retransmit-interval': KeyInfo(default='unspecified'), + }, + ), + ), + ('ipv6', 'nd', 'prefix'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + '6to4-interface': KeyInfo(default='none'), + 'autonomous': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(required=True), + 'on-link': KeyInfo(default=True), + 'preferred-lifetime': KeyInfo(), + 'prefix': KeyInfo(), + 'valid-lifetime': KeyInfo(), + }, + ), + ), + ('ipv6', 'nd', 'prefix', 'default'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'autonomous': KeyInfo(default=True), + 'preferred-lifetime': KeyInfo(default='1w'), + 'valid-lifetime': KeyInfo(default='4w2d'), + }, + ), + ), + ('ipv6', 'route'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'bgp-as-path': KeyInfo(can_disable=True), + 'bgp-atomic-aggregate': KeyInfo(can_disable=True), + 'bgp-communities': KeyInfo(can_disable=True), + 'bgp-local-pref': KeyInfo(can_disable=True), + 'bgp-med': KeyInfo(can_disable=True), + 'bgp-origin': KeyInfo(can_disable=True), + 'bgp-prepend': KeyInfo(can_disable=True), + 'type': KeyInfo(can_disable=True, remove_value='unicast'), + 'blackhole': KeyInfo(can_disable=True), + 'check-gateway': KeyInfo(can_disable=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'distance': KeyInfo(default=1), + 'dst-address': KeyInfo(), + 'gateway': KeyInfo(), + 'route-tag': KeyInfo(can_disable=True), + 'routing-table': KeyInfo(default='main'), + 'scope': KeyInfo(default=30), + 'target-scope': KeyInfo(default=10), + 'vrf-interface': KeyInfo(can_disable=True), + }, + ), + ), + ('mpls', ): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allow-fast-path': KeyInfo(default=True), + 'dynamic-label-range': KeyInfo(default='16-1048575'), + 'propagate-ttl': KeyInfo(default=True), + }, + ), + ), + ('mpls', 'interface'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'disabled': KeyInfo(default=False), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'interface': KeyInfo(required=True), + 'mpls-mtu': KeyInfo(), + 'info': KeyInfo(can_disable=True), + }, + ), + ), + ('mpls', 'ldp'): APIData( + versioned=[ + ('7.1', '>=', VersionedAPIData( + fully_understood=True, + primary_keys=('vrf', ), + fields={ + 'afi': KeyInfo(can_disable=True), + 'distribute-for-default': KeyInfo(can_disable=True), + 'path-vector-limit': KeyInfo(can_disable=True), + 'vrf': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'hop-limit': KeyInfo(can_disable=True), + 'preferred-afi': KeyInfo(can_disable=True), + 'loop-detect': KeyInfo(can_disable=True), + 'transport-addresses': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'lsr-id': KeyInfo(can_disable=True), + 'use-explicit-null': KeyInfo(can_disable=True), + }, + )), + ('7.1', '<', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'distribute-for-default-route': KeyInfo(default=False), + 'enabled': KeyInfo(default=False), + 'hop-limit': KeyInfo(default=255), + 'loop-detect': KeyInfo(default=False), + 'lsr-id': KeyInfo(default='0.0.0.0'), + 'path-vector-limit': KeyInfo(default=255), + 'transport-address': KeyInfo(default='0.0.0.0'), + 'use-explicit-null': KeyInfo(default=False), + }, + )), + ], + ), + ('mpls', 'ldp', 'accept-filter'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'accept': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'neighbor': KeyInfo(can_disable=True), + 'prefix': KeyInfo(can_disable=True), + 'vrf': KeyInfo(can_disable=True), + }, + ), + ), + ('mpls', 'ldp', 'advertise-filter'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'advertise': KeyInfo(default=''), + 'disabled': KeyInfo(default=False), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'neighbor': KeyInfo(), + 'prefix': KeyInfo(), + 'vrf': KeyInfo(), + }, + ), + ), + ('mpls', 'ldp', 'interface'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'disabled': KeyInfo(default=False), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'accept-dynamic-neighbors': KeyInfo(can_disable=True), + 'afi': KeyInfo(can_disable=True), + 'hello-interval': KeyInfo(can_disable=True), + 'hold-time': KeyInfo(can_disable=True), + 'interface': KeyInfo(required=True), + 'transport-addresses': KeyInfo(can_disable=True), + }, + ), + ), + ('port', 'firmware'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'directory': KeyInfo(default='firmware'), + 'ignore-directip-modem': KeyInfo(default=False), + }, + ), + ), + ('port', 'remote-access'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'allowed-addresses': KeyInfo(default='0.0.0.0/0'), + 'channel': KeyInfo(default=0), + 'disabled': KeyInfo(default=False), + 'log-file': KeyInfo(default=""), + 'port': KeyInfo(required=True), + 'protocol': KeyInfo(default='rfc2217'), + 'tcp-port': KeyInfo(default=0), + }, + ), + ), + ('ppp', 'aaa'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'accounting': KeyInfo(default=True), + 'interim-update': KeyInfo(default='0s'), + 'use-circuit-id-in-nas-port-id': KeyInfo(default=False), + 'use-radius': KeyInfo(default=False), + }, + ), + ), + ('radius', ): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'accounting-backup': KeyInfo(default=False), + 'accounting-port': KeyInfo(default=1813), + 'address': KeyInfo(default='0.0.0.0'), + 'authentication-port': KeyInfo(default=1812), + 'called-id': KeyInfo(), + 'certificate': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'domain': KeyInfo(), + 'protocol': KeyInfo(default='udp'), + 'realm': KeyInfo(), + 'secret': KeyInfo(), + 'service': KeyInfo(), + 'src-address': KeyInfo(default='0.0.0.0'), + 'timeout': KeyInfo(default='300ms'), + }, + versioned_fields=[ + ([('7.15', '>=')], 'require-message-auth', KeyInfo(default='yes-for-request-resp')), + ], + ), + ), + ('radius', 'incoming'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'accept': KeyInfo(default=False), + 'port': KeyInfo(default=3799), + }, + ), + ), + ('routing', 'id'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'id': KeyInfo(), + 'name': KeyInfo(), + 'select-dynamic-id': KeyInfo(), + 'select-from-vrf': KeyInfo(), + }, + ), + ), + ('routing', 'igmp-proxy'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'query-interval': KeyInfo(), + 'query-response-interval': KeyInfo(), + 'quick-leave': KeyInfo(default=False), + }, + ), + ), + ('routing', 'igmp-proxy', 'interface'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('interface', ), + fields={ + 'alternative-subnets': KeyInfo(default=''), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interface': KeyInfo(), + 'threshold': KeyInfo(), + 'upstream': KeyInfo(default=False), + }, + ), + ), + ('routing', 'bfd', 'configuration'): APIData( + versioned=[ + ('7.11', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'address-list': KeyInfo(), + 'addresses': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'copy-from': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'forbid-bfd': KeyInfo(), + 'interfaces': KeyInfo(), + 'min-echo-rx': KeyInfo(), + 'min-rx': KeyInfo(), + 'min-tx': KeyInfo(), + 'multiplier': KeyInfo(), + 'place-before': KeyInfo(), + 'vrf': KeyInfo(), + }, + )) + ], + ), + ('routing', 'bfd', 'interface'): APIData( + unversioned=VersionedAPIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'disabled': KeyInfo(), + 'interface': KeyInfo(), + 'interval': KeyInfo(), + 'min-rx': KeyInfo(), + 'multiplier': KeyInfo(), + }, + ), + ), + ('routing', 'mme'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'bidirectional-timeout': KeyInfo(default=2), + 'gateway-class': KeyInfo(default='none'), + 'gateway-keepalive': KeyInfo(default='1m'), + 'gateway-selection': KeyInfo(default='no-gateway'), + 'origination-interval': KeyInfo(default='5s'), + 'preferred-gateway': KeyInfo(default='0.0.0.0'), + 'timeout': KeyInfo(default='1m'), + 'ttl': KeyInfo(default=50), + }, + ), + ), + ('routing', 'rip'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'distribute-default': KeyInfo(default='never'), + 'garbage-timer': KeyInfo(default='2m'), + 'metric-bgp': KeyInfo(default=1), + 'metric-connected': KeyInfo(default=1), + 'metric-default': KeyInfo(default=1), + 'metric-ospf': KeyInfo(default=1), + 'metric-static': KeyInfo(default=1), + 'redistribute-bgp': KeyInfo(default=False), + 'redistribute-connected': KeyInfo(default=False), + 'redistribute-ospf': KeyInfo(default=False), + 'redistribute-static': KeyInfo(default=False), + 'routing-table': KeyInfo(default='main'), + 'timeout-timer': KeyInfo(default='3m'), + 'update-timer': KeyInfo(default='30s'), + }, + ), + ), + ('routing', 'ripng'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'distribute-default': KeyInfo(default='never'), + 'garbage-timer': KeyInfo(default='2m'), + 'metric-bgp': KeyInfo(default=1), + 'metric-connected': KeyInfo(default=1), + 'metric-default': KeyInfo(default=1), + 'metric-ospf': KeyInfo(default=1), + 'metric-static': KeyInfo(default=1), + 'redistribute-bgp': KeyInfo(default=False), + 'redistribute-connected': KeyInfo(default=False), + 'redistribute-ospf': KeyInfo(default=False), + 'redistribute-static': KeyInfo(default=False), + 'timeout-timer': KeyInfo(default='3m'), + 'update-timer': KeyInfo(default='30s'), + }, + ), + ), + ('snmp', ): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'contact': KeyInfo(default=''), + 'enabled': KeyInfo(default=False), + 'location': KeyInfo(default=''), + 'src-address': KeyInfo(default='::'), + 'trap-community': KeyInfo(default='public'), + 'trap-generators': KeyInfo(default='temp-exception'), + 'trap-target': KeyInfo(default=''), + 'trap-version': KeyInfo(default=1), + 'trap-interfaces': KeyInfo(default=''), + }, + versioned_fields=[ + ([('7.10', '<')], 'engine-id', KeyInfo(default='')), + ([('7.10', '>=')], 'engine-id', KeyInfo(read_only=True)), + ([('7.10', '>=')], 'engine-id-suffix', KeyInfo(default='')), + ], + ), + ), + ('system', 'clock'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'date': KeyInfo(), + 'gmt-offset': KeyInfo(), + 'time': KeyInfo(), + 'time-zone-autodetect': KeyInfo(default=True), + 'time-zone-name': KeyInfo(default='manual'), + }, + ), + ), + ('system', 'clock', 'manual'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'dst-delta': KeyInfo(default='00:00'), + 'dst-end': KeyInfo(default='jan/01/1970 00:00:00'), + 'dst-start': KeyInfo(default='jan/01/1970 00:00:00'), + 'time-zone': KeyInfo(default='+00:00'), + }, + ), + ), + ('system', 'health', 'settings'): APIData( + versioned=[ + ('7.14', '<', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'cpu-overtemp-check': KeyInfo(), + 'cpu-overtemp-startup-delay': KeyInfo(), + 'cpu-overtemp-threshold': KeyInfo(), + 'fan-control-interval': KeyInfo(can_disable=True, default='30s'), + 'fan-full-speed-temp': KeyInfo(default=65), + 'fan-min-speed-percent': KeyInfo(default=0), + 'fan-mode': KeyInfo(), + 'fan-on-threshold': KeyInfo(), + 'fan-switch': KeyInfo(), + 'fan-target-temp': KeyInfo(default=58), + 'use-fan': KeyInfo(), + }, + )), + ('7.14', '>=', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'cpu-overtemp-check': KeyInfo(), + 'cpu-overtemp-startup-delay': KeyInfo(), + 'cpu-overtemp-threshold': KeyInfo(), + 'fan-control-interval': KeyInfo(default=30), + 'fan-full-speed-temp': KeyInfo(default=65), + 'fan-min-speed-percent': KeyInfo(default=12), + 'fan-mode': KeyInfo(), + 'fan-on-threshold': KeyInfo(), + 'fan-switch': KeyInfo(), + 'fan-target-temp': KeyInfo(default=58), + 'use-fan': KeyInfo(), + }, + )), + ], + ), + ('system', 'identity'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'name': KeyInfo(default='Mikrotik'), + }, + ), + ), + ('system', 'leds', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'all-leds-off': KeyInfo(default='never'), + }, + ), + ), + ('system', 'note'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + versioned_fields=[ + ([('7.14', '>=')], 'show-at-cli-login', KeyInfo(default=False)), + ], + fields={ + 'note': KeyInfo(default=''), + 'show-at-login': KeyInfo(default=True), + }, + ), + ), + ('system', 'ntp', 'client'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'enabled': KeyInfo(default=False), + 'primary-ntp': KeyInfo(default='0.0.0.0'), + 'secondary-ntp': KeyInfo(default='0.0.0.0'), + 'server-dns-names': KeyInfo(default=''), + 'servers': KeyInfo(default=''), + 'mode': KeyInfo(default='unicast'), + 'vrf': KeyInfo(default='main'), + }, + ), + ), + ('system', 'ntp', 'client', 'servers'): APIData( + unversioned=VersionedAPIData( + primary_keys=('address', ), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'address': KeyInfo(), + 'auth-key': KeyInfo(default='none'), + 'iburst': KeyInfo(default=True), + 'max-poll': KeyInfo(default=10), + 'min-poll': KeyInfo(default=6), + }, + ), + ), + ('system', 'ntp', 'server'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'auth-key': KeyInfo(default='none'), + 'broadcast': KeyInfo(default=False), + 'broadcast-addresses': KeyInfo(default=''), + 'enabled': KeyInfo(default=False), + 'local-clock-stratum': KeyInfo(default=5), + 'manycast': KeyInfo(default=False), + 'multicast': KeyInfo(default=False), + 'use-local-clock': KeyInfo(default=False), + 'vrf': KeyInfo(default='main'), + }, + ), + ), + ('system', 'package', 'update'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'channel': KeyInfo(default='stable'), + 'installed-version': KeyInfo(read_only=True), + 'latest-version': KeyInfo(read_only=True), + 'status': KeyInfo(read_only=True), + }, + ), + ), + ('system', 'routerboard', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'auto-upgrade': KeyInfo(default=False), + 'baud-rate': KeyInfo(default=115200), + 'boot-delay': KeyInfo(default='2s'), + 'boot-device': KeyInfo(default='nand-if-fail-then-ethernet'), + 'boot-protocol': KeyInfo(default='bootp'), + 'cpu-frequency': KeyInfo(), + 'enable-jumper-reset': KeyInfo(default=True), + 'enter-setup-on': KeyInfo(default='any-key'), + 'force-backup-booter': KeyInfo(default=False), + 'memory-frequency': KeyInfo(), + 'preboot-etherboot': KeyInfo(), + 'preboot-etherboot-server': KeyInfo(), + 'protected-routerboot': KeyInfo(default='disabled'), + 'reformat-hold-button': KeyInfo(default='20s'), + 'reformat-hold-button-max': KeyInfo(default='10m'), + 'silent-boot': KeyInfo(default=False), + }, + ), + ), + ('system', 'upgrade', 'mirror'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'check-interval': KeyInfo(default='1d'), + 'enabled': KeyInfo(default=False), + 'primary-server': KeyInfo(default='0.0.0.0'), + 'secondary-server': KeyInfo(default='0.0.0.0'), + 'user': KeyInfo(default=''), + }, + ), + ), + ('system', 'ups'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'alarm-setting': KeyInfo(default='immediate'), + 'check-capabilities': KeyInfo(can_disable=True, remove_value=True), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=True), + 'min-runtime': KeyInfo(default='never'), + 'name': KeyInfo(), + 'offline-time': KeyInfo(default='0s'), + 'port': KeyInfo(required=True), + }, + ), + ), + ('system', 'watchdog'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'auto-send-supout': KeyInfo(default=False), + 'automatic-supout': KeyInfo(default=True), + 'ping-start-after-boot': KeyInfo(default='5m'), + 'ping-timeout': KeyInfo(default='1m'), + 'watch-address': KeyInfo(default='none'), + 'watchdog-timer': KeyInfo(default=True), + }, + ), + ), + ('tool', 'bandwidth-server'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allocate-udp-ports-from': KeyInfo(default=2000), + 'authenticate': KeyInfo(default=True), + 'enabled': KeyInfo(default=True), + 'max-sessions': KeyInfo(default=100), + }, + ), + ), + ('tool', 'e-mail'): APIData( + versioned=[ + ('7.12', '>=', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'from': KeyInfo(default='<>'), + 'password': KeyInfo(default=''), + 'port': KeyInfo(default=25), + 'server': KeyInfo(default='0.0.0.0'), + 'start-tls': KeyInfo(default=False), + 'tls': KeyInfo(default=False), + 'user': KeyInfo(default=''), + 'vfr': KeyInfo(default=''), + }, + )), + ('7.12', '<', VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'address': KeyInfo(default='0.0.0.0'), + 'from': KeyInfo(default='<>'), + 'password': KeyInfo(default=''), + 'port': KeyInfo(default=25), + 'start-tls': KeyInfo(default=False), + 'tls': KeyInfo(default=False), + 'user': KeyInfo(default=''), + 'vfr': KeyInfo(default=''), + }, + )), + ], + ), + ('tool', 'graphing'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'page-refresh': KeyInfo(default=300), + 'store-every': KeyInfo(default='5min'), + }, + ), + ), + ('tool', 'graphing', 'interface'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'allow-address': KeyInfo(default='0.0.0.0/0'), + 'interface': KeyInfo(default='all'), + 'store-on-disk': KeyInfo(default=True), + }, + )), + ], + ), + ('tool', 'graphing', 'resource'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'allow-address': KeyInfo(default='0.0.0.0/0'), + 'store-on-disk': KeyInfo(default=True), + }, + )), + ], + ), + ('tool', 'mac-server'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allowed-interface-list': KeyInfo(), + }, + ), + ), + ('tool', 'mac-server', 'mac-winbox'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allowed-interface-list': KeyInfo(), + }, + ), + ), + ('tool', 'mac-server', 'ping'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'enabled': KeyInfo(default=True), + }, + ), + ), + ('tool', 'netwatch'): APIData( + versioned=[ + ('7', '>=', VersionedAPIData( + fully_understood=True, + versioned_fields=[ + ([('7.16', '>=')], 'accept-icmp-time-exceeded', KeyInfo(default=False)), + ([('7.16', '>=')], 'ttl', KeyInfo(default=255)), + ], + fields={ + 'certificate': KeyInfo(), + 'check-certificate': KeyInfo(), + 'comment': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'down-script': KeyInfo(), + 'host': KeyInfo(required=True), + 'http-codes': KeyInfo(), + 'interval': KeyInfo(), + 'name': KeyInfo(), + 'packet-count': KeyInfo(), + 'packet-interval': KeyInfo(), + 'packet-size': KeyInfo(), + 'port': KeyInfo(), + 'src-address': KeyInfo(), + 'start-delay': KeyInfo(), + 'startup-delay': KeyInfo(), + 'test-script': KeyInfo(), + 'thr-avg': KeyInfo(), + 'thr-http-time': KeyInfo(), + 'thr-jitter': KeyInfo(), + 'thr-loss-count': KeyInfo(), + 'thr-loss-percent': KeyInfo(), + 'thr-max': KeyInfo(), + 'thr-stdev': KeyInfo(), + 'thr-tcp-conn-time': KeyInfo(), + 'timeout': KeyInfo(), + 'type': KeyInfo(default='simple'), + 'up-script': KeyInfo(), + }, + )), + ], + ), + ('tool', 'romon'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'enabled': KeyInfo(default=False), + 'id': KeyInfo(default='00:00:00:00:00:00'), + 'secrets': KeyInfo(default=''), + }, + ), + ), + ('tool', 'romon', 'port'): APIData( + unversioned=VersionedAPIData( + fields={ + 'cost': KeyInfo(), + 'disabled': KeyInfo(), + 'forbid': KeyInfo(), + 'interface': KeyInfo(), + 'secrets': KeyInfo(), + }, + ), + ), + ('tool', 'sms'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'allowed-number': KeyInfo(default=''), + 'auto-erase': KeyInfo(default=False), + 'channel': KeyInfo(default=0), + 'port': KeyInfo(default='none'), + 'receive-enabled': KeyInfo(default=False), + 'secret': KeyInfo(default=''), + 'sim-pin': KeyInfo(default=''), + }, + ), + ), + ('tool', 'sniffer'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'file-limit': KeyInfo(default='1000KiB'), + 'file-name': KeyInfo(default=''), + 'filter-cpu': KeyInfo(default=''), + 'filter-direction': KeyInfo(default='any'), + 'filter-interface': KeyInfo(default=''), + 'filter-ip-address': KeyInfo(default=''), + 'filter-ip-protocol': KeyInfo(default=''), + 'filter-ipv6-address': KeyInfo(default=''), + 'filter-mac-address': KeyInfo(default=''), + 'filter-mac-protocol': KeyInfo(default=''), + 'filter-operator-between-entries': KeyInfo(default='or'), + 'filter-port': KeyInfo(default=''), + 'filter-size': KeyInfo(default=''), + 'filter-stream': KeyInfo(default=False), + 'memory-limit': KeyInfo(default='100KiB'), + 'memory-scroll': KeyInfo(default=True), + 'only-headers': KeyInfo(default=False), + 'streaming-enabled': KeyInfo(default=False), + 'streaming-server': KeyInfo(default='0.0.0.0:37008'), + }, + ), + ), + ('tool', 'traffic-generator'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'latency-distribution-max': KeyInfo(default='100us'), + 'measure-out-of-order': KeyInfo(default=True), + 'stats-samples-to-keep': KeyInfo(default=100), + 'test-id': KeyInfo(default=0), + }, + ), + ), + ('user',): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'address': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'expired': KeyInfo(read_only=True), + 'group': KeyInfo(), + 'last-logged-in': KeyInfo(read_only=True), + 'name': KeyInfo(), + 'password': KeyInfo(write_only=True), + }, + ), + ), + ('user', 'aaa'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'accounting': KeyInfo(default=True), + 'default-group': KeyInfo(default='read'), + 'exclude-groups': KeyInfo(default=''), + 'interim-update': KeyInfo(default='0s'), + 'use-radius': KeyInfo(default=False), + }, + ), + ), + ('user', 'settings'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'minimum-categories': KeyInfo(), + 'minimum-password-length': KeyInfo(), + }, + ), + ), + ('queue', 'interface'): APIData( + unversioned=VersionedAPIData( + primary_keys=('interface', ), + fully_understood=True, + fixed_entries=True, + fields={ + 'interface': KeyInfo(required=True), + 'queue': KeyInfo(required=True), + }, + ), + ), + ('queue', 'simple'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'dst': KeyInfo(can_disable=True, remove_value=''), + 'time': KeyInfo(can_disable=True, remove_value=''), + 'bucket-size': KeyInfo(default='0.1/0.1'), + 'burst-limit': KeyInfo(default='0/0'), + 'burst-threshold': KeyInfo(default='0/0'), + 'burst-time': KeyInfo(default='0s/0s'), + 'disabled': KeyInfo(default=False), + 'limit-at': KeyInfo(default='0/0'), + 'max-limit': KeyInfo(default='0/0'), + 'name': KeyInfo(), + 'packet-marks': KeyInfo(default=''), + 'parent': KeyInfo(default='none'), + 'priority': KeyInfo(default='8/8'), + 'queue': KeyInfo(default='default-small/default-small'), + 'target': KeyInfo(required=True), + }, + ), + ), + ('queue', 'tree'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'bucket-size': KeyInfo(default='0.1'), + 'burst-limit': KeyInfo(default=0), + 'burst-threshold': KeyInfo(default=0), + 'burst-time': KeyInfo(default='0s'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'limit-at': KeyInfo(default=0), + 'max-limit': KeyInfo(default=0), + 'name': KeyInfo(), + 'packet-mark': KeyInfo(default=''), + 'parent': KeyInfo(required=True), + 'priority': KeyInfo(default=8), + 'queue': KeyInfo(default='default-small'), + }, + ), + ), + ('queue', 'type'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'name': KeyInfo(), + 'kind': KeyInfo(required=True), + 'bfifo-limit': KeyInfo(default=15000), + 'cake-ack-filter': KeyInfo(default='none'), + 'cake-atm': KeyInfo(default='none'), + 'cake-autorate-ingress': KeyInfo(can_disable=True), + 'cake-bandwidth': KeyInfo(can_disable=True, remove_value=0), + 'cake-diffserv': KeyInfo(default='diffserv3'), + 'cake-flowmode': KeyInfo(default='triple-isolate'), + 'cake-memlimit': KeyInfo(default=0), + 'cake-mpu': KeyInfo(can_disable=True, remove_value=''), + 'cake-nat': KeyInfo(can_disable=True, remove_value=False), + 'cake-overhead': KeyInfo(default=0), + 'cake-overhead-scheme': KeyInfo(can_disable=True, remove_value=''), + 'cake-rtt': KeyInfo(default='100ms'), + 'cake-rtt-scheme': KeyInfo(default='none'), + 'cake-wash': KeyInfo(can_disable=True, remove_value=False), + 'codel-ce-threshold': KeyInfo(can_disable=True, remove_value=''), + 'codel-ecn': KeyInfo(can_disable=True, remove_value=False), + 'codel-interval': KeyInfo(default='100ms'), + 'codel-limit': KeyInfo(default=1000), + 'codel-target': KeyInfo(default='5ms'), + 'fq-codel-ce-threshold': KeyInfo(can_disable=True, remove_value=''), + 'fq-codel-ecn': KeyInfo(default=True), + 'fq-codel-flows': KeyInfo(default=1024), + 'fq-codel-interval': KeyInfo(default='100ms'), + 'fq-codel-limit': KeyInfo(default=10240), + 'fq-codel-memlimit': KeyInfo(default=33554432), + 'fq-codel-quantum': KeyInfo(default=1514), + 'fq-codel-target': KeyInfo(default='5ms'), + 'mq-pfifo-limit': KeyInfo(default=50), + 'pcq-burst-rate': KeyInfo(default=0), + 'pcq-burst-threshold': KeyInfo(default=0), + 'pcq-burst-time': KeyInfo(default='10s'), + 'pcq-classifier': KeyInfo(can_disable=True, remove_value=''), + 'pcq-dst-address-mask': KeyInfo(default=32), + 'pcq-dst-address6-mask': KeyInfo(default=128), + 'pcq-limit': KeyInfo(default=50), + 'pcq-rate': KeyInfo(default=0), + 'pcq-src-address-mask': KeyInfo(default=32), + 'pcq-src-address6-mask': KeyInfo(default=128), + 'pcq-total-limit': KeyInfo(default=2000), + 'pfifo-limit': KeyInfo(default=50), + 'red-avg-packet': KeyInfo(default=1000), + 'red-burst': KeyInfo(default=20), + 'red-limit': KeyInfo(default=60), + 'red-max-threshold': KeyInfo(default=50), + 'red-min-threshold': KeyInfo(default=10), + 'sfq-allot': KeyInfo(default=1514), + 'sfq-perturb': KeyInfo(default=5), + }, + ), + ), + ('interface', 'ethernet', 'switch'): APIData( + unversioned=VersionedAPIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'cpu-flow-control': KeyInfo(default=True), + 'mirror-source': KeyInfo(default='none'), + 'mirror-target': KeyInfo(default='none'), + 'name': KeyInfo(), + }, + ), + ), + ('interface', 'ethernet', 'switch', 'port'): APIData( + unversioned=VersionedAPIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'default-vlan-id': KeyInfo(), + 'name': KeyInfo(), + 'vlan-header': KeyInfo(default='leave-as-is'), + 'vlan-mode': KeyInfo(default='disabled'), + }, + ), + ), + ('interface', 'ethernet', 'switch', 'port-isolation'): APIData( + versioned=[ + ('6.43', '>=', VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'forwarding-override': KeyInfo(), + 'name': KeyInfo(), + }, + )), + ], + ), + ('ip', 'dhcp-client', 'option'): APIData( + unversioned=VersionedAPIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'code': KeyInfo(), + 'name': KeyInfo(), + 'value': KeyInfo(), + }, + ), + ), + ('ppp', 'profile'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'address-list': KeyInfo(default=''), + 'bridge': KeyInfo(can_disable=True), + 'bridge-horizon': KeyInfo(can_disable=True), + 'bridge-learning': KeyInfo(default='default'), + 'bridge-path-cost': KeyInfo(can_disable=True), + 'bridge-port-priority': KeyInfo(can_disable=True), + 'change-tcp-mss': KeyInfo(default=True), + 'dns-server': KeyInfo(can_disable=True), + 'idle-timeout': KeyInfo(can_disable=True), + 'incoming-filter': KeyInfo(can_disable=True), + 'insert-queue-before': KeyInfo(can_disable=True), + 'interface-list': KeyInfo(can_disable=True), + 'local-address': KeyInfo(can_disable=True), + 'name': KeyInfo(required=True), + 'on-down': KeyInfo(default=''), + 'on-up': KeyInfo(default=''), + 'only-one': KeyInfo(default='default'), + 'outgoing-filter': KeyInfo(can_disable=True), + 'parent-queue': KeyInfo(can_disable=True), + 'queue-type': KeyInfo(can_disable=True), + 'rate-limit': KeyInfo(can_disable=True), + 'remote-address': KeyInfo(can_disable=True), + 'session-timeout': KeyInfo(can_disable=True), + 'use-compression': KeyInfo(default='default'), + 'use-encryption': KeyInfo(default='default'), + 'use-ipv6': KeyInfo(default=True), + 'use-mpls': KeyInfo(default='default'), + 'use-upnp': KeyInfo(default='default'), + 'wins-server': KeyInfo(can_disable=True), + }, + ), + ), + ('ppp', 'secret'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'caller-id': KeyInfo(default=''), + 'disabled': KeyInfo(default=False), + 'ipv6-routes': KeyInfo(default=''), + 'limit-bytes-in': KeyInfo(default=0), + 'limit-bytes-out': KeyInfo(default=0), + 'local-address': KeyInfo(can_disable=True), + 'name': KeyInfo(required=True), + 'password': KeyInfo(), + 'profile': KeyInfo(default='default'), + 'remote-address': KeyInfo(can_disable=True), + 'remote-ipv6-prefix': KeyInfo(can_disable=True), + 'routes': KeyInfo(can_disable=True), + 'service': KeyInfo(default='any'), + }, + ), + ), + ('routing', 'bgp', 'aggregate'): APIData( + unversioned=VersionedAPIData( + primary_keys=('prefix',), + fully_understood=True, + fields={ + 'advertise-filter': KeyInfo(), + 'attribute-filter': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'include-igp': KeyInfo(default=False), + 'inherit-attributes': KeyInfo(default=True), + 'instance': KeyInfo(required=True), + 'prefix': KeyInfo(required=True), + 'summary-only': KeyInfo(default=True), + 'suppress-filter': KeyInfo(), + }, + ), + ), + ('routing', 'bgp', 'connection'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + versioned_fields=[ + ([('7.19', '<')], 'address-families', KeyInfo()), + ([('7.19', '>=')], 'afi', KeyInfo()), + ], + fields={ + 'as': KeyInfo(), + 'add-path-out': KeyInfo(), + 'cisco-vpls-nlri-len-fmt': KeyInfo(), + 'cluster-id': KeyInfo(), + 'comment': KeyInfo(), + 'connect': KeyInfo(default=True), + 'disabled': KeyInfo(default=False), + 'hold-time': KeyInfo(), + 'input.accept-communities': KeyInfo(), + 'input.accept-ext-communities': KeyInfo(), + 'input.accept-large-communities': KeyInfo(), + 'input.accpet-nlri': KeyInfo(), + 'input.accept-unknown': KeyInfo(), + 'input.affinity': KeyInfo(), + 'input.allow-as': KeyInfo(), + 'input.filter': KeyInfo(), + 'input.ignore-as-path-len': KeyInfo(), + 'input.limit-process-routes-ipv4': KeyInfo(), + 'input.limit-process-routes-ipv6': KeyInfo(), + 'keepalive-time': KeyInfo(), + 'listen': KeyInfo(default=True), + 'local.address': KeyInfo(), + 'local.port': KeyInfo(), + 'local.role': KeyInfo(required=True), + 'local.ttl': KeyInfo(), + 'multihop': KeyInfo(), + 'name': KeyInfo(required=True), + 'nexthop-choice': KeyInfo(), + 'output.affinity': KeyInfo(), + 'output.as-override': KeyInfo(), + 'output.default-originate': KeyInfo(), + 'output.default-prepend': KeyInfo(), + 'output.filter-chain': KeyInfo(), + 'output.filter-select': KeyInfo(), + 'output.keep-sent-attributes': KeyInfo(), + 'output.network': KeyInfo(), + 'output.no-client-to-client-reflection': KeyInfo(), + 'output.no-early-cut': KeyInfo(), + 'output.redistribute': KeyInfo(), + 'output.remote-private-as': KeyInfo(), + 'remote.address': KeyInfo(required=True), + 'remote.port': KeyInfo(), + 'remote.as': KeyInfo(), + 'remote.allowed-as': KeyInfo(), + 'remote.ttl': KeyInfo(), + 'router-id': KeyInfo(), + 'routing-table': KeyInfo(), + 'save-to': KeyInfo(), + 'tcp-md5-key': KeyInfo(), + 'templates': KeyInfo(), + 'use-bfd': KeyInfo(), + 'vrf': KeyInfo(), + }, + ), + ), + ('routing', 'bgp', 'instance'): APIData( + unversioned=VersionedAPIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'as': KeyInfo(), + 'client-to-client-reflection': KeyInfo(), + 'cluster-id': KeyInfo(can_disable=True), + 'confederation': KeyInfo(can_disable=True), + 'disabled': KeyInfo(default=False), + 'ignore-as-path-len': KeyInfo(), + 'name': KeyInfo(), + 'out-filter': KeyInfo(), + 'redistribute-connected': KeyInfo(), + 'redistribute-ospf': KeyInfo(), + 'redistribute-other-bgp': KeyInfo(), + 'redistribute-rip': KeyInfo(), + 'redistribute-static': KeyInfo(), + 'router-id': KeyInfo(), + 'routing-table': KeyInfo(), + }, + ), + ), + ('routing', 'bgp', 'network'): APIData( + unversioned=VersionedAPIData( + primary_keys=('network',), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'network': KeyInfo(required=True), + 'synchronize': KeyInfo(default=True), + }, + ), + ), + ('routing', 'bgp', 'peer'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'address-families': KeyInfo(default='ip'), + 'allow-as-in': KeyInfo(can_disable=True, remove_value=''), + 'as-override': KeyInfo(default=False), + 'cisco-vpls-nlri-len-fmt': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'default-originate': KeyInfo(default='never'), + 'disabled': KeyInfo(default=False), + 'hold-time': KeyInfo(default='3m'), + 'in-filter': KeyInfo(), + 'instance': KeyInfo(), + 'keepalive-time': KeyInfo(can_disable=True, remove_value=''), + 'max-prefix-limit': KeyInfo(can_disable=True, remove_value=''), + 'max-prefix-restart-time': KeyInfo(can_disable=True, remove_value=''), + 'multihop': KeyInfo(default=False), + 'name': KeyInfo(), + 'nexthop-choice': KeyInfo(default='default'), + 'passive': KeyInfo(default=False), + 'out-filter': KeyInfo(), + 'remote-address': KeyInfo(required=True), + 'remote-as': KeyInfo(required=True), + 'remote-port': KeyInfo(can_disable=True, remove_value=''), + 'remove-private-as': KeyInfo(default=False), + 'route-reflect': KeyInfo(default=False), + 'tcp-md5-key': KeyInfo(), + 'ttl': KeyInfo(default='default'), + 'update-source': KeyInfo(can_disable=True, remove_value='none'), + 'use-bfd': KeyInfo(default=False), + }, + ), + ), + ('routing', 'bgp', 'template'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'add-path-out': KeyInfo(), + 'address-families': KeyInfo(default='ip'), + 'as': KeyInfo(), + 'as-override': KeyInfo(default=False), + 'cisco-vpls-nlri-len-fmt': KeyInfo(), + 'cluster-id': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'hold-time': KeyInfo(default='3m'), + 'input.accept-communities': KeyInfo(), + 'input.accept-ext-communities': KeyInfo(), + 'input.accept-large-communities': KeyInfo(), + 'input.accept-unknown': KeyInfo(), + 'input.accept-nlri': KeyInfo(), + 'input.affinity': KeyInfo(), + 'input.allow-as': KeyInfo(), + 'input.filter': KeyInfo(), + 'input.ignore-as-path-len': KeyInfo(default=False), + 'input.limit-nlri-diversity': KeyInfo(), + 'input.limit-process-routes-ipv4': KeyInfo(), + 'input.limit-process-routes-ipv6': KeyInfo(), + 'keepalive-time': KeyInfo(default='3m'), + 'multihop': KeyInfo(default=False), + 'name': KeyInfo(), + 'nexthop-choice': KeyInfo(default='default'), + 'output.affinity': KeyInfo(), + 'output.default-originate': KeyInfo(default='never'), + 'output.default-prepent': KeyInfo(), + 'output.filter-chain': KeyInfo(), + 'output.filter-select': KeyInfo(), + 'output.keep-sent-attributes': KeyInfo(default=False), + 'output.network': KeyInfo(), + 'output.no-client-to-client-reflection': KeyInfo(), + 'output.no-early-cut': KeyInfo(), + 'output.redistribute': KeyInfo(), + 'remove-private-as': KeyInfo(default=False), + 'router-id': KeyInfo(default='main'), + 'routing-table': KeyInfo(default='main'), + 'save-to': KeyInfo(), + 'templates': KeyInfo(), + 'use-bfd': KeyInfo(default=False), + 'vrf': KeyInfo(default='main'), + }, + ), + ), + ('system', 'logging', 'action'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + versioned_fields=[ + ([('7.18', '>=')], 'remote-log-format', KeyInfo(default='default')), + ([('7.18', '>=')], 'remote-protocol', KeyInfo(default='udp')), + ([('7.18', '>=')], 'cef-event-delimiter', KeyInfo(default='\r\n')), + ], + fields={ + 'bsd-syslog': KeyInfo(default=False), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disk-file-count': KeyInfo(default=2), + 'disk-file-name': KeyInfo(default='log'), + 'disk-lines-per-file': KeyInfo(default=1000), + 'disk-stop-on-full': KeyInfo(default=False), + 'email-start-tls': KeyInfo(default=False), + 'email-to': KeyInfo(default=''), + 'memory-lines': KeyInfo(default=1000), + 'memory-stop-on-full': KeyInfo(default=False), + 'name': KeyInfo(), + 'remember': KeyInfo(default=True), + 'remote': KeyInfo(default='0.0.0.0'), + 'remote-port': KeyInfo(default=514), + 'src-address': KeyInfo(default='0.0.0.0'), + 'syslog-facility': KeyInfo(default='daemon'), + 'syslog-severity': KeyInfo(default='auto'), + 'syslog-time-format': KeyInfo(default='bsd-syslog'), + 'target': KeyInfo(required=True), + }, + ), + ), + ('user', 'group'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'name': KeyInfo(), + 'policy': KeyInfo(), + 'skin': KeyInfo(default='default'), + }, + ), + ), + ('caps-man', 'manager'): APIData( + unversioned=VersionedAPIData( + single_value=True, + fully_understood=True, + fields={ + 'ca-certificate': KeyInfo(default='none'), + 'certificate': KeyInfo(default='none'), + 'enabled': KeyInfo(default=False), + 'package-path': KeyInfo(default=''), + 'require-peer-certificate': KeyInfo(default=False), + 'upgrade-policy': KeyInfo(default='none'), + }, + ), + ), + ('ip', 'firewall', 'service-port'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + 'ports': KeyInfo(), + 'sip-direct-media': KeyInfo(), + 'sip-timeout': KeyInfo(), + }, + ), + ), + ('ip', 'firewall', 'layer7-protocol'): APIData( + unversioned=VersionedAPIData( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'name': KeyInfo(), + 'regexp': KeyInfo(), + }, + ), + ), + ('ip', 'hotspot', 'service-port'): APIData( + unversioned=VersionedAPIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + 'ports': KeyInfo(), + }, + ), + ), + ('ip', 'ipsec', 'policy'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(default='encrypt'), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'dst-address': KeyInfo(), + 'dst-port': KeyInfo(default='any'), + 'group': KeyInfo(can_disable=True, remove_value='default'), + 'ipsec-protocols': KeyInfo(default='esp'), + 'level': KeyInfo(default='require'), + 'peer': KeyInfo(), + 'proposal': KeyInfo(default='default'), + 'protocol': KeyInfo(default='all'), + 'src-address': KeyInfo(), + 'src-port': KeyInfo(default='any'), + # The template field ca not really be changed once the item is + # created. This config captures the behavior best as it can + # i.e. template=yes is shown, template=no is hidden. + 'template': KeyInfo(can_disable=True, remove_value=False), + 'tunnel': KeyInfo(default=False), + }, + ), + ), + ('ip', 'service'): APIData( + unversioned=VersionedAPIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + versioned_fields=[ + ([('7.16', '>=')], 'max-sessions', KeyInfo(default=20)), + ], + fields={ + 'address': KeyInfo(), + 'certificate': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + 'port': KeyInfo(), + 'tls-version': KeyInfo(), + }, + ), + ), + ('system', 'logging'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + fields={ + 'action': KeyInfo(default='memory'), + 'disabled': KeyInfo(default=False), + 'prefix': KeyInfo(default=''), + 'topics': KeyInfo(default=''), + }, + ), + ), + ('system', 'resource', 'irq'): APIData( + unversioned=VersionedAPIData( + has_identifier=True, + fields={ + 'cpu': KeyInfo(), + }, + ), + ), + ('system', 'resource', 'irq', 'rps'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + fields={ + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + }, + ), + ), + ('system', 'scheduler'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interval': KeyInfo(default='0s'), + 'name': KeyInfo(), + 'on-event': KeyInfo(default=''), + 'policy': KeyInfo(default='ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon'), + 'start-date': KeyInfo(), + 'start-time': KeyInfo(), + }, + ), + ), + ('system', 'script'): APIData( + unversioned=VersionedAPIData( + fully_understood=True, + primary_keys=('name',), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'dont-require-permissions': KeyInfo(default=False), + 'name': KeyInfo(), + 'owner': KeyInfo(), + 'policy': KeyInfo(default='ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon'), + 'source': KeyInfo(default=''), + }, + ), + ), +} diff --git a/plugins/module_utils/_api_helper.py b/plugins/module_utils/_api_helper.py new file mode 100644 index 0000000..ffd897f --- /dev/null +++ b/plugins/module_utils/_api_helper.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# The data inside here is private to this collection. If you use this from outside the collection, +# you are on your own. There can be random changes to its format even in bugfix releases! + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re + +from ansible.module_utils.common.text.converters import to_text + + +def validate_and_prepare_restrict(module, path_info): + restrict = module.params['restrict'] + if restrict is None: + return None + restrict_data = [] + for rule in restrict: + field = rule['field'] + if field.startswith('!'): + module.fail_json(msg='restrict: the field name "{0}" must not start with "!"'.format(field)) + f = path_info.fields.get(field) + if f is None: + module.fail_json(msg='restrict: the field "{0}" does not exist for this path'.format(field)) + + new_rule = dict( + field=field, + match_disabled=rule['match_disabled'], + invert=rule['invert'], + ) + if rule['values'] is not None: + new_rule['values'] = rule['values'] + if rule['regex'] is not None: + regex = rule['regex'] + try: + new_rule['regex'] = re.compile(regex) + new_rule['regex_source'] = regex + except Exception as exc: + module.fail_json(msg='restrict: invalid regular expression "{0}": {1}'.format(regex, exc)) + restrict_data.append(new_rule) + return restrict_data + + +def _value_to_str(value): + if value is None: + return None + value_str = to_text(value) + if isinstance(value, bool): + value_str = value_str.lower() + return value_str + + +def _test_rule_except_invert(value, rule): + if value is None and rule['match_disabled']: + return True + if 'values' in rule and value in rule['values']: + return True + if 'regex' in rule and value is not None and rule['regex'].match(_value_to_str(value)): + return True + return False + + +def restrict_entry_accepted(entry, path_info, restrict_data): + if restrict_data is None: + return True + for rule in restrict_data: + # Obtain field and value + field = rule['field'] + field_info = path_info.fields[field] + value = entry.get(field) + if value is None: + value = field_info.default + if field not in entry and field_info.absent_value: + value = field_info.absent_value + + # Check + matches_rule = _test_rule_except_invert(value, rule) + if rule['invert']: + matches_rule = not matches_rule + if not matches_rule: + return False + return True + + +def restrict_argument_spec(): + return dict( + restrict=dict( + type='list', + elements='dict', + options=dict( + field=dict(type='str', required=True), + match_disabled=dict(type='bool', default=False), + values=dict(type='list', elements='raw'), + regex=dict(type='str'), + invert=dict(type='bool', default=False), + ), + ), + ) diff --git a/plugins/module_utils/api.py b/plugins/module_utils/api.py new file mode 100644 index 0000000..aa3aa2e --- /dev/null +++ b/plugins/module_utils/api.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein (@felixfontein) +# Copyright (c) 2020, Nikolay Dachev +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native + +import ssl +import traceback + +LIB_IMP_ERR = None +try: + from librouteros import connect + from librouteros.exceptions import LibRouterosError # noqa: F401, pylint: disable=unused-import + HAS_LIB = True +except Exception as e: + HAS_LIB = False + LIB_IMP_ERR = traceback.format_exc() + + +def check_has_library(module): + if not HAS_LIB: + module.fail_json( + msg=missing_required_lib('librouteros'), + exception=LIB_IMP_ERR, + ) + + +def api_argument_spec(): + return dict( + username=dict(type='str', required=True), + password=dict(type='str', required=True, no_log=True), + hostname=dict(type='str', required=True), + port=dict(type='int'), + tls=dict(type='bool', default=False, aliases=['ssl']), + force_no_cert=dict(type='bool', default=False), + validate_certs=dict(type='bool', default=True), + validate_cert_hostname=dict(type='bool', default=False), + ca_path=dict(type='path'), + encoding=dict(type='str', default='ASCII'), + timeout=dict(type='int', default=10), + ) + + +def _ros_api_connect(module, username, password, host, port, use_tls, force_no_cert, validate_certs, validate_cert_hostname, ca_path, encoding, timeout): + '''Connect to RouterOS API.''' + if not port: + if use_tls: + port = 8729 + else: + port = 8728 + try: + params = dict( + username=username, + password=password, + host=host, + port=port, + encoding=encoding, + timeout=timeout, + ) + if use_tls: + ctx = ssl.create_default_context(cafile=ca_path) + wrap_context = ctx.wrap_socket + if force_no_cert: + ctx.check_hostname = False + ctx.set_ciphers("ADH:@SECLEVEL=0") + elif not validate_certs: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + elif not validate_cert_hostname: + ctx.check_hostname = False + else: + # Since librouteros does not pass server_hostname, + # we have to do this ourselves: + def wrap_context(*args, **kwargs): + kwargs.pop('server_hostname', None) + return ctx.wrap_socket(*args, server_hostname=host, **kwargs) + params['ssl_wrapper'] = wrap_context + api = connect(**params) + except Exception as e: + connection = { + 'username': username, + 'hostname': host, + 'port': port, + 'ssl': use_tls, + 'status': 'Error while connecting: %s' % to_native(e), + } + module.fail_json(msg=connection['status'], connection=connection) + return api + + +def create_api(module): + """Create an API object.""" + return _ros_api_connect( + module, + module.params['username'], + module.params['password'], + module.params['hostname'], + module.params['port'], + module.params['tls'], + module.params['force_no_cert'], + module.params['validate_certs'], + module.params['validate_cert_hostname'], + module.params['ca_path'], + module.params['encoding'], + module.params['timeout'], + ) + + +def get_api_version(api): + """Given an API object, query the system's version.""" + system_info = list(api.path().join('system', 'resource'))[0] + return system_info['version'].split(' ', 1)[0] diff --git a/plugins/module_utils/quoting.py b/plugins/module_utils/quoting.py new file mode 100644 index 0000000..4b70989 --- /dev/null +++ b/plugins/module_utils/quoting.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import sys + +from ansible.module_utils.common.text.converters import to_native, to_bytes + + +class ParseError(Exception): + pass + + +ESCAPE_SEQUENCES = { + b'"': b'"', + b'\\': b'\\', + b'?': b'?', + b'$': b'$', + b'_': b' ', + b'a': b'\a', + b'b': b'\b', + b'f': b'\xFF', + b'n': b'\n', + b'r': b'\r', + b't': b'\t', + b'v': b'\v', +} + +ESCAPE_SEQUENCE_REVERSED = dict([(v, k) for k, v in ESCAPE_SEQUENCES.items()]) + +ESCAPE_DIGITS = b'0123456789ABCDEF' + + +if sys.version_info[0] < 3: + _int_to_byte = chr +else: + def _int_to_byte(value): + return bytes((value, )) + + +def parse_argument_value(line, start_index=0, must_match_everything=True): + ''' + Parse an argument value (quoted or not quoted) from ``line``. + + Will start at offset ``start_index``. Returns pair ``(parsed_value, + end_index)``, where ``end_index`` is the first character after the + attribute. + + If ``must_match_everything`` is ``True`` (default), will fail if + ``end_index < len(line)``. + ''' + line = to_bytes(line) + length = len(line) + index = start_index + if index == length: + raise ParseError('Expected value, but found end of string') + quoted = False + if line[index:index + 1] == b'"': + quoted = True + index += 1 + current = [] + while index < length: + ch = line[index:index + 1] + index += 1 + if not quoted and ch == b' ': + index -= 1 + break + elif ch == b'"': + if quoted: + quoted = False + if line[index:index + 1] not in (b'', b' '): + raise ParseError('Ending \'"\' must be followed by space or end of string') + break + raise ParseError('\'"\' must not appear in an unquoted value') + elif ch == b'\\': + if not quoted: + raise ParseError('Escape sequences can only be used inside double quotes') + if index == length: + raise ParseError('\'\\\' must not be at the end of the line') + ch = line[index:index + 1] + index += 1 + if ch in ESCAPE_SEQUENCES: + current.append(ESCAPE_SEQUENCES[ch]) + else: + d1 = ESCAPE_DIGITS.find(ch) + if d1 < 0: + raise ParseError('Invalid escape sequence \'\\{0}\''.format(to_native(ch))) + if index == length: + raise ParseError('Hex escape sequence cut off at end of line') + ch2 = line[index:index + 1] + d2 = ESCAPE_DIGITS.find(ch2) + index += 1 + if d2 < 0: + raise ParseError('Invalid hex escape sequence \'\\{0}\''.format(to_native(ch + ch2))) + current.append(_int_to_byte(d1 * 16 + d2)) + else: + if not quoted and ch in (b"'", b'=', b'(', b')', b'$', b'[', b'{', b'`'): + raise ParseError('"{0}" can only be used inside double quotes'.format(to_native(ch))) + if ch == b'?': + raise ParseError('"{0}" can only be used in escaped form'.format(to_native(ch))) + current.append(ch) + if quoted: + raise ParseError('Unexpected end of string during escaped parameter') + if must_match_everything and index < length: + raise ParseError('Unexpected data at end of value') + return to_native(b''.join(current)), index + + +def split_routeros_command(line): + line = to_bytes(line) + result = [] + current = [] + index = 0 + length = len(line) + parsing_attribute_name = False + while index < length: + ch = line[index:index + 1] + index += 1 + if ch == b' ': + if parsing_attribute_name: + parsing_attribute_name = False + result.append(b''.join(current)) + current = [] + elif ch == b'=' and parsing_attribute_name: + current.append(ch) + value, index = parse_argument_value(line, start_index=index, must_match_everything=False) + current.append(to_bytes(value)) + parsing_attribute_name = False + result.append(b''.join(current)) + current = [] + elif ch in (b'"', b'\\', b"'", b'=', b'(', b')', b'$', b'[', b'{', b'`', b'?'): + raise ParseError('Found unexpected "{0}"'.format(to_native(ch))) + else: + current.append(ch) + parsing_attribute_name = True + if parsing_attribute_name and current: + result.append(b''.join(current)) + return [to_native(part) for part in result] + + +def quote_routeros_argument_value(argument): + argument = to_bytes(argument) + result = [] + quote = False + length = len(argument) + index = 0 + while index < length: + letter = argument[index:index + 1] + index += 1 + if letter in ESCAPE_SEQUENCE_REVERSED: + result.append(b'\\%s' % ESCAPE_SEQUENCE_REVERSED[letter]) + quote = True + continue + elif ord(letter) < 32: + v = ord(letter) + v1 = v % 16 + v2 = v // 16 + result.append(b'\\%s%s' % (ESCAPE_DIGITS[v2:v2 + 1], ESCAPE_DIGITS[v1:v1 + 1])) + quote = True + continue + elif letter in (b' ', b'=', b';', b"'"): + quote = True + result.append(letter) + argument = to_native(b''.join(result)) + if quote or not argument: + argument = '"%s"' % argument + return argument + + +def quote_routeros_argument(argument): + def check_attribute(attribute): + if ' ' in attribute: + raise ParseError('Attribute names must not contain spaces') + return attribute + + if '=' not in argument: + check_attribute(argument) + return argument + + attribute, value = argument.split('=', 1) + check_attribute(attribute) + value = quote_routeros_argument_value(value) + return '%s=%s' % (attribute, value) + + +def join_routeros_command(arguments): + return ' '.join([quote_routeros_argument(argument) for argument in arguments]) + + +def convert_list_to_dictionary(string_list, require_assignment=True, skip_empty_values=False): + dictionary = {} + for p in string_list: + if '=' not in p: + if require_assignment: + raise ParseError("missing '=' after '%s'" % p) + dictionary[p] = None + continue + p = p.split('=', 1) + if not skip_empty_values or p[1]: + dictionary[p[0]] = p[1] + return dictionary diff --git a/plugins/module_utils/routeros.py b/plugins/module_utils/routeros.py index e0488b2..c2bd09c 100644 --- a/plugins/module_utils/routeros.py +++ b/plugins/module_utils/routeros.py @@ -1,30 +1,6 @@ -# This code is part of Ansible, but is an independent component. -# This particular file snippet, and this file snippet only, is BSD licensed. -# Modules you write using this snippet, which is embedded dynamically by Ansible -# still belong to the author of the module, and may assign their own license -# to the complete work. -# -# (c) 2016 Red Hat Inc. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# +# Copyright (c) 2016 Red Hat Inc. +# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause) +# SPDX-License-Identifier: BSD-2-Clause from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -33,6 +9,7 @@ import json from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.basic import env_fallback from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, ComplexList +from ansible_collections.community.routeros.plugins.module_utils.version import LooseVersion from ansible.module_utils.connection import Connection, ConnectionError _DEVICE_CONFIGS = {} @@ -127,6 +104,16 @@ def to_commands(module, commands): return transform(commands) +def should_add_leading_space(module): + """Determines whether adding a leading space to the command is needed + to workaround prompt bug in 6.49 <= ROS < 7.2""" + capabilities = get_capabilities(module) + network_os_version = capabilities.get('device_info', {}).get('network_os_version') + if network_os_version is None: + return False + return LooseVersion('6.49') <= LooseVersion(network_os_version) < LooseVersion('7.2') + + def run_commands(module, commands, check_rc=True): responses = list() connection = get_connection(module) @@ -141,6 +128,9 @@ def run_commands(module, commands, check_rc=True): prompt = None answer = None + if should_add_leading_space(module): + command = " " + command + try: out = connection.get(command, prompt, answer) except ConnectionError as exc: diff --git a/plugins/module_utils/version.py b/plugins/module_utils/version.py new file mode 100644 index 0000000..cc3028c --- /dev/null +++ b/plugins/module_utils/version.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Provide version object to compare version numbers.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.module_utils.compat.version import LooseVersion # pylint: disable=unused-import diff --git a/plugins/modules/api.py b/plugins/modules/api.py index c25e6c8..d3ec089 100644 --- a/plugins/modules/api.py +++ b/plugins/modules/api.py @@ -1,318 +1,347 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: (c) 2020, Nikolay Dachev +# Copyright (c) 2020, Nikolay Dachev # GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt +# SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) __metaclass__ = type -DOCUMENTATION = ''' ---- +DOCUMENTATION = r""" module: api author: "Nikolay Dachev (@NikolayDachev)" short_description: Ansible module for RouterOS API description: - - Ansible module for RouterOS API with python librouteros. - - This module can add, remove, update, query and execute arbitrary command in routeros via API. + - Ansible module for RouterOS API with the Python C(librouteros) library. + - This module can add, remove, update, query, and execute arbitrary command in RouterOS through the API. notes: - - I(add), I(remove), I(update), I(cmd) and I(query) are mutually exclusive. - - I(check_mode) is not supported. -requirements: - - librouteros - - Python >= 3.6 (for librouteros) + - O(add), O(remove), O(update), O(cmd), and O(query) are mutually exclusive. + - Use the M(community.routeros.api_modify) and M(community.routeros.api_find_and_modify) modules for more specific modifications, + and the M(community.routeros.api_info) module for a more controlled way of returning all entries for a path. +extends_documentation_fragment: + - community.routeros.api + - community.routeros.attributes + - community.routeros.attributes.actiongroup_api +attributes: + check_mode: + support: none + diff_mode: + support: none + platform: + support: full + platforms: RouterOS + action_group: + version_added: 2.1.0 + idempotent: + support: N/A + details: + - Whether the executed command is idempotent depends on the operation performed. options: - hostname: - description: - - RouterOS hostname API. - required: true - type: str - username: - description: - - RouterOS login user. - required: true - type: str - password: - description: - - RouterOS user password. - required: true - type: str - tls: - description: - - If is set TLS will be used for RouterOS API connection. - required: false - type: bool - default: false - aliases: - - ssl - port: - description: - - RouterOS api port. If I(tls) is set, port will apply to TLS/SSL connection. - - Defaults are C(8728) for the HTTP API, and C(8729) for the HTTPS API. - type: int path: description: - Main path for all other arguments. - - If other arguments are not set, api will return all items in selected path. - - Example C(ip address). Equivalent of RouterOS CLI C(/ip address print). + - If other arguments are not set, the module will return all items in selected path. + - Example V(ip address). Equivalent of RouterOS CLI C(/ip address print). required: true type: str add: description: - Will add selected arguments in selected path to RouterOS config. - - Example C(address=1.1.1.1/32 interface=ether1). + - Example V(address=1.1.1.1/32 interface=ether1). - Equivalent in RouterOS CLI C(/ip address add address=1.1.1.1/32 interface=ether1). type: str remove: description: - Remove config/value from RouterOS by '.id'. - - Example C(*03) will remove config/value with C(id=*03) in selected path. + - Example V(*03) will remove config/value with C(id=*03) in selected path. - Equivalent in RouterOS CLI C(/ip address remove numbers=1). - Note C(number) in RouterOS CLI is different from C(.id). type: str update: description: - Update config/value in RouterOS by '.id' in selected path. - - Example C(.id=*03 address=1.1.1.3/32) and path C(ip address) will replace existing ip address with C(.id=*03). + - Example V(.id=*03 address=1.1.1.3/32) and path V(ip address) will replace the existing IP address with C(.id=*03). - Equivalent in RouterOS CLI C(/ip address set address=1.1.1.3/32 numbers=1). - Note C(number) in RouterOS CLI is different from C(.id). type: str query: description: - - Query given path for selected query attributes from RouterOS aip and return '.id'. + - Query given path for selected query attributes from RouterOS API. - WHERE is key word which extend query. WHERE format is key operator value - with spaces. - - WHERE valid operators are C(==), C(!=), C(>), C(<). - - Example path C(ip address) and query C(.id address) will return only C(.id) and C(address) for all items in C(ip address) path. - - Example path C(ip address) and query C(.id address WHERE address == 1.1.1.3/32). - will return only C(.id) and C(address) for items in C(ip address) path, where address is eq to 1.1.1.3/32. - - Example path C(interface) and query C(mtu name WHERE mut > 1400) will - return only interfaces C(mtu,name) where mtu is bigger than 1400. + - WHERE valid operators are V(==) or V(eq), V(!=) or V(not), V(>) or V(more), V(<) or V(less). + - Example path V(ip address) and query V(.id address) will return only C(.id) and C(address) for all items in V(ip address) + path. + - Example path V(ip address) and query V(.id address WHERE address == 1.1.1.3/32). will return only C(.id) and C(address) + for items in V(ip address) path, where address is eq to 1.1.1.3/32. + - Example path V(interface) and query V(mtu name WHERE mut > 1400) will return only interfaces C(mtu,name) where mtu + is bigger than 1400. - Equivalent in RouterOS CLI C(/interface print where mtu > 1400). type: str + extended_query: + description: + - Extended query given path for selected query attributes from RouterOS API. + - Extended query allow conjunctive input. If there is no matching entry, an empty list will be returned. + type: dict + suboptions: + attributes: + description: + - The list of attributes to return. + - Every attribute used in a O(extended_query.where[]) clause need to be listed here. + type: list + elements: str + required: true + where: + description: + - Allows to restrict the objects returned. + - The conditions here must all match. An O(extended_query.where[].or) condition needs at least one of its conditions + to match. + type: list + elements: dict + suboptions: + attribute: + description: + - The attribute to match. Must be part of O(extended_query.attributes). + - Either O(extended_query.where[].or) or all of O(extended_query.where[].attribute), O(extended_query.where[].is), + and O(extended_query.where[].value) have to be specified. + type: str + is: + description: + - The operator to use for matching. + - For equality use V(==) or V(eq). For less use V(<) or V(less). For more use V(>) or V(more). + - Use V(in) to check whether the value is part of a list. In that case, O(extended_query.where[].value) must + be a list. + - Either O(extended_query.where[].or) or all of O(extended_query.where[].attribute), O(extended_query.where[].is), + and O(extended_query.where[].value) have to be specified. + type: str + choices: ["==", "!=", ">", "<", "in", "eq", "not", "more", "less"] + value: + description: + - The value to compare to. Must be a list for O(extended_query.where[].is=in). + - Either O(extended_query.where[].or) or all of O(extended_query.where[].attribute), O(extended_query.where[].is), + and O(extended_query.where[].value) have to be specified. + type: raw + or: + description: + - A list of conditions so that at least one of them has to match. + - Either O(extended_query.where[].or) or all of O(extended_query.where[].attribute), O(extended_query.where[].is), + and O(extended_query.where[].value) have to be specified. + type: list + elements: dict + suboptions: + attribute: + description: + - The attribute to match. Must be part of O(extended_query.attributes). + type: str + required: true + is: + description: + - The operator to use for matching. + - For equality use V(==) or V(eq). For less use V(<) or V(less). For more use V(>) or V(more). + - Use V(in) to check whether the value is part of a list. In that case, O(extended_query.where[].or[].value) + must be a list. + type: str + choices: ["==", "!=", ">", "<", "in", "eq", "not", "more", "less"] + required: true + value: + description: + - The value to compare to. Must be a list for O(extended_query.where[].or[].is=in). + type: raw + required: true cmd: description: - Execute any/arbitrary command in selected path, after the command we can add C(.id). - - Example path C(system script) and cmd C(run .id=*03) is equivalent in RouterOS CLI C(/system script run number=0). - - Example path C(ip address) and cmd C(print) is equivalent in RouterOS CLI C(/ip address print). + - Example path V(system script) and cmd V(run .id=*03) is equivalent in RouterOS CLI C(/system script run number=0). + - Example path V(ip address) and cmd V(print) is equivalent in RouterOS CLI C(/ip address print). type: str - validate_certs: - description: - - Set to C(false) to skip validation of TLS certificates. - - See also I(validate_cert_hostname). Only used when I(tls=true). - - B(Note:) instead of simply deactivating certificate validations to "make things work", - please consider creating your own CA certificate and using it to sign certificates used - for your router. You can tell the module about your CA certificate with the I(ca_path) - option. - type: bool - default: true - version_added: 1.2.0 - validate_cert_hostname: - description: - - Set to C(true) to validate hostnames in certificates. - - See also I(validate_certs). Only used when I(tls=true) and I(validate_certs=true). - type: bool - default: false - version_added: 1.2.0 - ca_path: - description: - - PEM formatted file that contains a CA certificate to be used for certificate validation. - - See also I(validate_cert_hostname). Only used when I(tls=true) and I(validate_certs=true). - type: path - version_added: 1.2.0 -''' +seealso: + - ref: ansible_collections.community.routeros.docsite.quoting + description: How to quote and unquote commands and arguments. + - module: community.routeros.api_facts + - module: community.routeros.api_find_and_modify + - module: community.routeros.api_info + - module: community.routeros.api_modify +""" -EXAMPLES = ''' +EXAMPLES = r""" --- -- name: Use RouterOS API - hosts: localhost - gather_facts: no - vars: - hostname: "ros_api_hostname/ip" - username: "admin" - password: "secret_password" - +- name: Get example - ip address print + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" path: "ip address" + register: ipaddrd_printout - nic: "ether2" - ip1: "1.1.1.1/32" - ip2: "2.2.2.2/32" - ip3: "3.3.3.3/32" +- name: Dump "Get example" output + ansible.builtin.debug: + msg: '{{ ipaddrd_printout }}' - tasks: - - name: Get "{{ path }} print" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "{{ path }}" - register: print_path +- name: Add example - ip address + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + add: "address=192.168.255.10/24 interface=ether2" - - name: Dump "{{ path }} print" output - ansible.builtin.debug: - msg: '{{ print_path }}' +- name: Query example - ".id, address" in "ip address WHERE address == 192.168.255.10/24" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + query: ".id address WHERE address == {{ ip2 }}" + register: queryout - - name: Add ip address "{{ ip1 }}" and "{{ ip2 }}" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "{{ path }}" - add: "{{ item }}" - loop: - - "address={{ ip1 }} interface={{ nic }}" - - "address={{ ip2 }} interface={{ nic }}" - register: addout +- name: Dump "Query example" output + ansible.builtin.debug: + msg: '{{ queryout }}' - - name: Dump "Add ip address" output - ".id" for new added items - ansible.builtin.debug: - msg: '{{ addout }}' +- name: Extended query example - ".id,address,network" where address is not 192.168.255.10/24 or is 10.20.36.20/24 + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + extended_query: + attributes: + - network + - address + - .id + where: + - attribute: "network" + is: "==" + value: "192.168.255.0" + - or: + - attribute: "address" + is: "!=" + value: "192.168.255.10/24" + - attribute: "address" + is: "eq" + value: "10.20.36.20/24" + - attribute: "network" + is: "in" + value: + - "10.20.36.0" + - "192.168.255.0" + register: extended_queryout - - name: Query for ".id" in "{{ path }} WHERE address == {{ ip2 }}" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "{{ path }}" - query: ".id address WHERE address == {{ ip2 }}" - register: queryout +- name: Dump "Extended query example" output + ansible.builtin.debug: + msg: '{{ extended_queryout }}' - - name: Dump "Query for" output and set fact with ".id" for "{{ ip2 }}" - ansible.builtin.debug: - msg: '{{ queryout }}' +- name: Update example - ether2 ip address with ".id = *14" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + update: >- + .id=*14 + address=192.168.255.20/24 + comment={{ 'Update 192.168.255.10/24 to 192.168.255.20/24 on ether2' | community.routeros.quote_argument_value }} - - name: Store query_id for later usage - ansible.builtin.set_fact: - query_id: "{{ queryout['msg'][0]['.id'] }}" +- name: Remove example - ether2 ip 192.168.255.20/24 with ".id = *14" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + remove: "*14" - - name: Update ".id = {{ query_id }}" taken with custom fact "fquery_id" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "{{ path }}" - update: ".id={{ query_id }} address={{ ip3 }}" - register: updateout +- name: Arbitrary command example "/system identity print" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "system identity" + cmd: "print" + register: arbitraryout - - name: Dump "Update" output - ansible.builtin.debug: - msg: '{{ updateout }}' +- name: Dump "Arbitrary command example" output + ansible.builtin.debug: + msg: '{{ arbitraryout }}' +""" - - name: Remove ips - stage 1 - query ".id" for "{{ ip2 }}" and "{{ ip3 }}" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "{{ path }}" - query: ".id address WHERE address == {{ item }}" - register: id_to_remove - loop: - - "{{ ip2 }}" - - "{{ ip3 }}" - - - name: Set fact for ".id" from "Remove ips - stage 1 - query" - ansible.builtin.set_fact: - to_be_remove: "{{ to_be_remove |default([]) + [item['msg'][0]['.id']] }}" - loop: "{{ id_to_remove.results }}" - - - name: Dump "Remove ips - stage 1 - query" output - ansible.builtin.debug: - msg: '{{ to_be_remove }}' - - # Remove "{{ rmips }}" with ".id" by "to_be_remove" from query - - name: Remove ips - stage 2 - remove "{{ ip2 }}" and "{{ ip3 }}" by '.id' - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "{{ path }}" - remove: "{{ item }}" - register: remove - loop: "{{ to_be_remove }}" - - - name: Dump "Remove ips - stage 2 - remove" output - ansible.builtin.debug: - msg: '{{ remove }}' - - - name: Arbitrary command example "/system identity print" - community.routeros.api: - hostname: "{{ hostname }}" - password: "{{ password }}" - username: "{{ username }}" - path: "system identity" - cmd: "print" - register: cmdout - - - name: Dump "Arbitrary command example" output - ansible.builtin.debug: - msg: "{{ cmdout }}" -''' - -RETURN = ''' ---- +RETURN = r""" message: - description: All outputs are in list with dictionary elements returned from RouterOS api. - sample: C([{...},{...}]) - type: list - returned: always -''' + description: All outputs are in list with dictionary elements returned from RouterOS API. + sample: + - address: 1.2.3.4 + - address: 2.3.4.5 + type: list + returned: always +""" from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.common.text.converters import to_native -import ssl -import traceback +from ansible_collections.community.routeros.plugins.module_utils.quoting import ( + ParseError, + convert_list_to_dictionary, + parse_argument_value, + split_routeros_command, +) + +from ansible_collections.community.routeros.plugins.module_utils.api import ( + api_argument_spec, + check_has_library, + create_api, +) + +import re -LIB_IMP_ERR = None try: - from librouteros import connect from librouteros.exceptions import LibRouterosError - from librouteros.query import Key - HAS_LIB = True -except Exception as e: - HAS_LIB = False - LIB_IMP_ERR = traceback.format_exc() + from librouteros.query import Key, Or +except Exception: + # Handled in api module_utils + pass class ROS_api_module: def __init__(self): module_args = dict( - username=dict(type='str', required=True), - password=dict(type='str', required=True, no_log=True), - hostname=dict(type='str', required=True), - port=dict(type='int'), - tls=dict(type='bool', default=False, aliases=['ssl']), path=dict(type='str', required=True), add=dict(type='str'), remove=dict(type='str'), update=dict(type='str'), cmd=dict(type='str'), query=dict(type='str'), - validate_certs=dict(type='bool', default=True), - validate_cert_hostname=dict(type='bool', default=False), - ca_path=dict(type='path'), + extended_query=dict(type='dict', options=dict( + attributes=dict(type='list', elements='str', required=True), + where=dict( + type='list', + elements='dict', + options={ + 'attribute': dict(type='str'), + 'is': dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"]), + 'value': dict(type='raw'), + 'or': dict(type='list', elements='dict', options={ + 'attribute': dict(type='str', required=True), + 'is': dict(type='str', choices=["==", "!=", ">", "<", "in", "eq", "not", "more", "less"], required=True), + 'value': dict(type='raw', required=True), + }), + }, + required_together=[('attribute', 'is', 'value')], + mutually_exclusive=[('attribute', 'or')], + required_one_of=[('attribute', 'or')], + ), + )), ) + module_args.update(api_argument_spec()) self.module = AnsibleModule(argument_spec=module_args, supports_check_mode=False, mutually_exclusive=(('add', 'remove', 'update', - 'cmd', 'query'),),) + 'cmd', 'query', 'extended_query'),),) - if not HAS_LIB: - self.module.fail_json(msg=missing_required_lib("librouteros"), - exception=LIB_IMP_ERR) + check_has_library(self.module) - self.api = self.ros_api_connect(self.module.params['username'], - self.module.params['password'], - self.module.params['hostname'], - self.module.params['port'], - self.module.params['tls'], - self.module.params['validate_certs'], - self.module.params['validate_cert_hostname'], - self.module.params['ca_path'], - ) + self.api = create_api(self.module) - self.path = self.list_remove_empty(self.module.params['path'].split(' ')) + self.path = self.module.params['path'].split() self.add = self.module.params['add'] self.remove = self.module.params['remove'] self.update = self.module.params['update'] @@ -320,13 +349,7 @@ class ROS_api_module: self.where = None self.query = self.module.params['query'] - if self.query: - if 'WHERE' in self.query: - split = self.query.split('WHERE') - self.query = self.list_remove_empty(split[0].split(' ')) - self.where = self.list_remove_empty(split[1].split(' ')) - else: - self.query = self.list_remove_empty(self.module.params['query'].split(' ')) + self.extended_query = self.module.params['extended_query'] self.result = dict( message=[]) @@ -334,34 +357,79 @@ class ROS_api_module: # create api base path self.api_path = self.api_add_path(self.api, self.path) - # api call's - if self.add: - self.api_add() - elif self.remove: - self.api_remove() - elif self.update: - self.api_update() - elif self.query: - self.api_query() - elif self.arbitrary: - self.api_arbitrary() - else: - self.api_get_all() + # api calls + try: + if self.add: + self.api_add() + elif self.remove: + self.api_remove() + elif self.update: + self.api_update() + elif self.query: + self.check_query() + self.api_query() + elif self.extended_query: + self.check_extended_query() + self.api_extended_query() + elif self.arbitrary: + self.api_arbitrary() + else: + self.api_get_all() + except UnicodeEncodeError as exc: + self.module.fail_json(msg='Error while encoding text: {error}'.format(error=exc)) - def list_remove_empty(self, check_list): - while("" in check_list): - check_list.remove("") - return check_list + def check_query(self): + where_index = self.query.find(' WHERE ') + if where_index < 0: + self.query = self.split_params(self.query) + else: + where = self.query[where_index + len(' WHERE '):] + self.query = self.split_params(self.query[:where_index]) + # where must be of the format ' ' + m = re.match(r'^\s*([^ ]+)\s+([^ ]+)\s+(.*)$', where) + if not m: + self.errors("invalid syntax for 'WHERE %s'" % where) + try: + self.where = [ + m.group(1), # attribute + m.group(2), # operator + parse_argument_value(m.group(3).rstrip())[0], # value + ] + except ParseError as exc: + self.errors("invalid syntax for 'WHERE %s': %s" % (where, exc)) + try: + idx = self.query.index('WHERE') + self.where = self.query[idx + 1:] + self.query = self.query[:idx] + except ValueError: + # Raised when WHERE has not been found + pass + + def check_extended_query_syntax(self, test_atr, or_msg=''): + if test_atr['is'] == "in" and not isinstance(test_atr['value'], list): + self.errors("invalid syntax 'extended_query':'where':%s%s 'value' must be a type list" % (or_msg, test_atr)) + + def check_extended_query(self): + if self.extended_query["where"]: + for i in self.extended_query['where']: + if i["or"] is not None: + if len(i['or']) < 2: + self.errors("invalid syntax 'extended_query':'where':'or':%s 'or' requires minimum two items" % i["or"]) + for orv in i['or']: + self.check_extended_query_syntax(orv, ":'or':") + else: + self.check_extended_query_syntax(i) def list_to_dic(self, ldict): - dict = {} - for p in ldict: - if '=' not in p: - self.errors("missing '=' after '%s'" % p) - p = p.split('=') - if p[1]: - dict[p[0]] = p[1] - return dict + return convert_list_to_dictionary(ldict, skip_empty_values=True, require_assignment=True) + + def split_params(self, params): + if not isinstance(params, str): + raise AssertionError('Parameters can only be a string, received %s' % type(params)) + try: + return split_routeros_command(params) + except ParseError as e: + self.module.fail_json(msg=to_native(e)) def api_add_path(self, api, path): api_path = api.path() @@ -378,7 +446,7 @@ class ROS_api_module: self.errors(e) def api_add(self): - param = self.list_to_dic(self.add.split(' ')) + param = self.list_to_dic(self.split_params(self.add)) try: self.result['message'].append("added: .id= %s" % self.api_path.add(**param)) @@ -395,7 +463,7 @@ class ROS_api_module: self.errors(e) def api_update(self): - param = self.list_to_dic(self.update.split(' ')) + param = self.list_to_dic(self.split_params(self.update)) if '.id' not in param.keys(): self.errors("missing '.id' for %s" % param) try: @@ -413,27 +481,21 @@ class ROS_api_module: keys[k] = Key(k) try: if self.where: - if len(self.where) < 3: - self.errors("invalid syntax for 'WHERE %s'" - % ' '.join(self.where)) - - where = [] - if self.where[1] == '==': + if self.where[1] in ('==', 'eq'): select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) - elif self.where[1] == '!=': + elif self.where[1] in ('!=', 'not'): select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) - elif self.where[1] == '>': + elif self.where[1] in ('>', 'more'): select = self.api_path.select(*keys).where(keys[self.where[0]] > self.where[2]) - elif self.where[1] == '<': + elif self.where[1] in ('<', 'less'): select = self.api_path.select(*keys).where(keys[self.where[0]] < self.where[2]) else: self.errors("'%s' is not operator for 'where'" % self.where[1]) - for row in select: - self.result['message'].append(row) else: - for row in self.api_path.select(*keys): - self.result['message'].append(row) + select = self.api_path.select(*keys) + for row in select: + self.result['message'].append(row) if len(self.result['message']) < 1: msg = "no results for '%s 'query' %s" % (' '.join(self.path), ' '.join(self.query)) @@ -444,9 +506,52 @@ class ROS_api_module: except LibRouterosError as e: self.errors(e) + def build_api_extended_query(self, item): + if item['attribute'] not in self.extended_query['attributes']: + self.errors("'%s' attribute is not in attributes: %s" + % (item, self.extended_query['attributes'])) + if item['is'] in ('eq', '=='): + return self.query_keys[item['attribute']] == item['value'] + elif item['is'] in ('not', '!='): + return self.query_keys[item['attribute']] != item['value'] + elif item['is'] in ('less', '<'): + return self.query_keys[item['attribute']] < item['value'] + elif item['is'] in ('more', '>'): + return self.query_keys[item['attribute']] > item['value'] + elif item['is'] == 'in': + return self.query_keys[item['attribute']].In(*item['value']) + else: + self.errors("'%s' is not operator for 'is'" % item['is']) + + def api_extended_query(self): + self.query_keys = {} + for k in self.extended_query['attributes']: + if k == 'id': + self.errors("'extended_query':'attributes':'%s' must be '.id'" % k) + self.query_keys[k] = Key(k) + try: + if self.extended_query['where']: + where_args = [] + for i in self.extended_query['where']: + if i['or']: + where_or_args = [] + for ior in i['or']: + where_or_args.append(self.build_api_extended_query(ior)) + where_args.append(Or(*where_or_args)) + else: + where_args.append(self.build_api_extended_query(i)) + select = self.api_path.select(*self.query_keys).where(*where_args) + else: + select = self.api_path.select(*self.extended_query['attributes']) + for row in select: + self.result['message'].append(row) + self.return_result(False) + except LibRouterosError as e: + self.errors(e) + def api_arbitrary(self): param = {} - self.arbitrary = self.arbitrary.split(' ') + self.arbitrary = self.split_params(self.arbitrary) arb_cmd = self.arbitrary[0] if len(self.arbitrary) > 1: param = self.list_to_dic(self.arbitrary[1:]) @@ -472,49 +577,6 @@ class ROS_api_module: self.result['message'].append("%s" % e) self.return_result(False, False) - def ros_api_connect(self, username, password, host, port, use_tls, validate_certs, validate_cert_hostname, ca_path): - # connect to routeros api - conn_status = {"connection": {"username": username, - "hostname": host, - "port": port, - "ssl": use_tls, - "status": "Connected"}} - try: - if use_tls: - if not port: - port = 8729 - conn_status["connection"]["port"] = port - ctx = ssl.create_default_context(cafile=ca_path) - wrap_context = ctx.wrap_socket - if not validate_certs: - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - elif not validate_cert_hostname: - ctx.check_hostname = False - else: - # Since librouteros doesn't pass server_hostname, - # we have to do this ourselves: - def wrap_context(*args, **kwargs): - kwargs.pop('server_hostname', None) - return ctx.wrap_socket(*args, server_hostname=host, **kwargs) - api = connect(username=username, - password=password, - host=host, - ssl_wrapper=wrap_context, - port=port) - else: - if not port: - port = 8728 - conn_status["connection"]["port"] = port - api = connect(username=username, - password=password, - host=host, - port=port) - except Exception as e: - conn_status["connection"]["status"] = "error: %s" % e - self.module.fail_json(msg=to_native([conn_status])) - return api - def main(): diff --git a/plugins/modules/api_facts.py b/plugins/modules/api_facts.py new file mode 100644 index 0000000..d0f6b00 --- /dev/null +++ b/plugins/modules/api_facts.py @@ -0,0 +1,492 @@ +#!/usr/bin/python + +# Copyright (c) 2022, Felix Fontein +# Copyright (c) 2020, Nikolay Dachev +# Copyright (c) 2018, Egor Zaitsev (@heuels) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r""" +module: api_facts +author: + - "Egor Zaitsev (@heuels)" + - "Nikolay Dachev (@NikolayDachev)" + - "Felix Fontein (@felixfontein)" +version_added: 2.1.0 +short_description: Collect facts from remote devices running MikroTik RouterOS using the API +description: + - Collects a base set of device facts from a remote device that is running RouterOS. This module prepends all of the base + network fact keys with C(ansible_net_). The facts module will always collect a base set of facts from the device + and can enable or disable collection of additional facts. + - As opposed to the M(community.routeros.facts) module, it uses the RouterOS API, similar to the M(community.routeros.api) + module. +extends_documentation_fragment: + - community.routeros.api + - community.routeros.attributes + - community.routeros.attributes.actiongroup_api + - community.routeros.attributes.facts + - community.routeros.attributes.facts_module + - community.routeros.attributes.idempotent_not_modify_state +attributes: + platform: + support: full + platforms: RouterOS +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected to a given subset. Possible values for this argument + include V(all), V(hardware), V(interfaces), and V(routing). + - Can specify a list of values to include a larger subset. Values can also be used with an initial V(!) to specify that + a specific subset should not be collected. + required: false + default: + - all + type: list + elements: str +seealso: + - module: community.routeros.facts + - module: community.routeros.api + - module: community.routeros.api_find_and_modify + - module: community.routeros.api_info + - module: community.routeros.api_modify +""" + +EXAMPLES = r""" +--- +- name: Collect all facts from the device + community.routeros.api_facts: + hostname: 192.168.88.1 + username: admin + password: password + gather_subset: all + +- name: Do not collect hardware facts + community.routeros.api_facts: + hostname: 192.168.88.1 + username: admin + password: password + gather_subset: + - "!hardware" +""" + +RETURN = r""" +ansible_facts: + description: "Dictionary of IP geolocation facts for a host's IP address." + returned: always + type: dict + contains: + ansible_net_gather_subset: + description: The list of fact subsets collected from the device. + returned: always + type: list + + # default + ansible_net_model: + description: The model name returned from the device. + returned: O(gather_subset) contains V(default) + type: str + ansible_net_serialnum: + description: The serial number of the remote device. + returned: O(gather_subset) contains V(default) + type: str + ansible_net_version: + description: The operating system version running on the remote device. + returned: O(gather_subset) contains V(default) + type: str + ansible_net_hostname: + description: The configured hostname of the device. + returned: O(gather_subset) contains V(default) + type: str + ansible_net_arch: + description: The CPU architecture of the device. + returned: O(gather_subset) contains V(default) + type: str + ansible_net_uptime: + description: The uptime of the device. + returned: O(gather_subset) contains V(default) + type: str + ansible_net_cpu_load: + description: Current CPU load. + returned: O(gather_subset) contains V(default) + type: str + + # hardware + ansible_net_spacefree_mb: + description: The available disk space on the remote device in MiB. + returned: O(gather_subset) contains V(hardware) + type: dict + ansible_net_spacetotal_mb: + description: The total disk space on the remote device in MiB. + returned: O(gather_subset) contains V(hardware) + type: dict + ansible_net_memfree_mb: + description: The available free memory on the remote device in MiB. + returned: O(gather_subset) contains V(hardware) + type: int + ansible_net_memtotal_mb: + description: The total memory on the remote device in MiB. + returned: O(gather_subset) contains V(hardware) + type: int + + # interfaces + ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device. + returned: O(gather_subset) contains V(interfaces) + type: list + ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device. + returned: O(gather_subset) contains V(interfaces) + type: list + ansible_net_interfaces: + description: A hash of all interfaces running on the system. + returned: O(gather_subset) contains V(interfaces) + type: dict + ansible_net_neighbors: + description: The list of neighbors from the remote device. + returned: O(gather_subset) contains V(interfaces) + type: dict + + # routing + ansible_net_bgp_peer: + description: A dictionary with BGP peer information. + returned: O(gather_subset) contains V(routing) + type: dict + ansible_net_bgp_vpnv4_route: + description: A dictionary with BGP vpnv4 route information. + returned: O(gather_subset) contains V(routing) + type: dict + ansible_net_bgp_instance: + description: A dictionary with BGP instance information. + returned: O(gather_subset) contains V(routing) + type: dict + ansible_net_route: + description: A dictionary for routes in all routing tables. + returned: O(gather_subset) contains V(routing) + type: dict + ansible_net_ospf_instance: + description: A dictionary with OSPF instances. + returned: O(gather_subset) contains V(routing) + type: dict + ansible_net_ospf_neighbor: + description: A dictionary with OSPF neighbors. + returned: O(gather_subset) contains V(routing) + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.routeros.plugins.module_utils.api import ( + api_argument_spec, + check_has_library, + create_api, +) + +try: + from librouteros.exceptions import LibRouterosError +except Exception: + # Handled in api module_utils + pass + + +class FactsBase(object): + + COMMANDS = [] + + def __init__(self, module, api): + self.module = module + self.api = api + self.facts = {} + self.responses = None + + def populate(self): + self.responses = [] + for path in self.COMMANDS: + self.responses.append(self.query_path(path)) + + def query_path(self, path): + api_path = self.api.path() + for part in path: + api_path = api_path.join(part) + try: + return list(api_path) + except LibRouterosError as e: + self.module.warn('Error while querying path {path}: {error}'.format( + path=' '.join(path), + error=to_native(e), + )) + return [] + + +class Default(FactsBase): + + COMMANDS = [ + ['system', 'identity'], + ['system', 'resource'], + ['system', 'routerboard'], + ] + + def populate(self): + super(Default, self).populate() + data = self.responses[0] + if data: + self.facts['hostname'] = data[0].get('name') + data = self.responses[1] + if data: + self.facts['version'] = data[0].get('version') + self.facts['arch'] = data[0].get('architecture-name') + self.facts['uptime'] = data[0].get('uptime') + self.facts['cpu_load'] = data[0].get('cpu-load') + data = self.responses[2] + if data: + self.facts['model'] = data[0].get('model') + self.facts['serialnum'] = data[0].get('serial-number') + + +class Hardware(FactsBase): + + COMMANDS = [ + ['system', 'resource'], + ] + + def populate(self): + super(Hardware, self).populate() + data = self.responses[0] + if data: + self.parse_filesystem_info(data[0]) + self.parse_memory_info(data[0]) + + def parse_filesystem_info(self, data): + self.facts['spacefree_mb'] = self.to_megabytes(data.get('free-hdd-space')) + self.facts['spacetotal_mb'] = self.to_megabytes(data.get('total-hdd-space')) + + def parse_memory_info(self, data): + self.facts['memfree_mb'] = self.to_megabytes(data.get('free-memory')) + self.facts['memtotal_mb'] = self.to_megabytes(data.get('total-memory')) + + def to_megabytes(self, value): + if value is None: + return None + return float(value) / 1024 / 1024 + + +class Interfaces(FactsBase): + + COMMANDS = [ + ['interface'], + ['ip', 'address'], + ['ipv6', 'address'], + ['ip', 'neighbor'], + ] + + def populate(self): + super(Interfaces, self).populate() + + self.facts['interfaces'] = {} + self.facts['all_ipv4_addresses'] = [] + self.facts['all_ipv6_addresses'] = [] + self.facts['neighbors'] = [] + + data = self.responses[0] + if data: + interfaces = self.parse_interfaces(data) + self.populate_interfaces(interfaces) + + data = self.responses[1] + if data: + data = self.parse_detail(data) + self.populate_addresses(data, 'ipv4') + + data = self.responses[2] + if data: + data = self.parse_detail(data) + self.populate_addresses(data, 'ipv6') + + data = self.responses[3] + if data: + self.facts['neighbors'] = list(self.parse_detail(data)) + + def populate_interfaces(self, data): + for key, value in iteritems(data): + self.facts['interfaces'][key] = value + + def populate_addresses(self, data, family): + for value in data: + key = value['interface'] + iface = self.facts['interfaces'].setdefault(key, ( + {"type": "ansible:unknown"} if key.startswith('*') else + {"type": "ansible:mismatch"})) + iface_addrs = iface.setdefault(family, []) + addr, subnet = value['address'].split('/') + subnet = subnet.strip() + # Try to convert subnet to an integer + try: + subnet = int(subnet) + except Exception: + pass + ip = dict(address=addr.strip(), subnet=subnet) + self.add_ip_address(addr.strip(), family) + iface_addrs.append(ip) + + def add_ip_address(self, address, family): + if family == 'ipv4': + self.facts['all_ipv4_addresses'].append(address) + else: + self.facts['all_ipv6_addresses'].append(address) + + def parse_interfaces(self, data): + facts = {} + for entry in data: + if 'name' not in entry: + continue + entry.pop('.id', None) + facts[entry['name']] = entry + return facts + + def parse_detail(self, data): + for entry in data: + if 'interface' not in entry: + continue + entry.pop('.id', None) + yield entry + + +class Routing(FactsBase): + + COMMANDS = [ + ['routing', 'bgp', 'peer'], + ['routing', 'bgp', 'vpnv4-route'], + ['routing', 'bgp', 'instance'], + ['ip', 'route'], + ['routing', 'ospf', 'instance'], + ['routing', 'ospf', 'neighbor'], + ] + + def populate(self): + super(Routing, self).populate() + self.facts['bgp_peer'] = {} + self.facts['bgp_vpnv4_route'] = {} + self.facts['bgp_instance'] = {} + self.facts['route'] = {} + self.facts['ospf_instance'] = {} + self.facts['ospf_neighbor'] = {} + data = self.responses[0] + if data: + peer = self.parse(data, 'name') + self.populate_result('bgp_peer', peer) + data = self.responses[1] + if data: + vpnv4 = self.parse(data, 'interface') + self.populate_result('bgp_vpnv4_route', vpnv4) + data = self.responses[2] + if data: + instance = self.parse(data, 'name') + self.populate_result('bgp_instance', instance) + data = self.responses[3] + if data: + route = self.parse(data, 'routing-mark', fallback='main') + self.populate_result('route', route) + data = self.responses[4] + if data: + instance = self.parse(data, 'name') + self.populate_result('ospf_instance', instance) + data = self.responses[5] + if data: + instance = self.parse(data, 'instance') + self.populate_result('ospf_neighbor', instance) + + def parse(self, data, key, fallback=None): + facts = {} + for line in data: + name = line.get(key) or fallback + line.pop('.id', None) + facts[name] = line + return facts + + def populate_result(self, name, data): + for key, value in iteritems(data): + self.facts[name][key] = value + + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + routing=Routing, +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + + +def main(): + argument_spec = dict( + gather_subset=dict( + default=['all'], + type='list', + elements='str', + ) + ) + argument_spec.update(api_argument_spec()) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + check_has_library(module) + api = create_api(module) + + gather_subset = module.params['gather_subset'] + + runable_subsets = set() + exclude_subsets = set() + + for subset in gather_subset: + if subset == 'all': + runable_subsets.update(VALID_SUBSETS) + continue + + if subset.startswith('!'): + subset = subset[1:] + if subset == 'all': + exclude_subsets.update(VALID_SUBSETS) + continue + exclude = True + else: + exclude = False + + if subset not in VALID_SUBSETS: + module.fail_json(msg='Bad subset: %s' % subset) + + if exclude: + exclude_subsets.add(subset) + else: + runable_subsets.add(subset) + + if not runable_subsets: + runable_subsets.update(VALID_SUBSETS) + + runable_subsets.difference_update(exclude_subsets) + runable_subsets.add('default') + + facts = {} + facts['gather_subset'] = sorted(runable_subsets) + + instances = [] + for key in runable_subsets: + instances.append(FACT_SUBSETS[key](module, api)) + + for inst in instances: + inst.populate() + facts.update(inst.facts) + + ansible_facts = {} + for key, value in iteritems(facts): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/api_find_and_modify.py b/plugins/modules/api_find_and_modify.py new file mode 100644 index 0000000..acdd125 --- /dev/null +++ b/plugins/modules/api_find_and_modify.py @@ -0,0 +1,367 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein +# GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r""" +module: api_find_and_modify +author: + - "Felix Fontein (@felixfontein)" +short_description: Find and modify information using the API +version_added: 2.1.0 +description: + - Allows to find entries for a path by conditions and modify the values of these entries. + - Use the M(community.routeros.api_find_and_modify) module to set all entries of a path to specific values, or change multiple + entries in different ways in one step. +notes: + - "If you want to change values based on their old values (like change all comments 'foo' to 'bar') and make sure that there + are at least N such values, you can use O(require_matches_min=N) together with O(allow_no_matches=true). This will make + the module fail if there are less than N such entries, but not if there is no match. The latter case is needed for idempotency + of the task: once the values have been changed, there should be no further match." +extends_documentation_fragment: + - community.routeros.api + - community.routeros.attributes + - community.routeros.attributes.actiongroup_api +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + support: full + platforms: RouterOS + idempotent: + support: full +options: + path: + description: + - Path to query. + - An example value is V(ip address). This is equivalent to running C(/ip address) in the RouterOS CLI. + required: true + type: str + find: + description: + - Fields to search for. + - The module will only consider entries in the given O(path) that match all fields provided here. + - Use YAML V(~), or prepend keys with V(!), to specify an unset value. + - Note that if the dictionary specified here is empty, every entry in the path will be matched. + required: true + type: dict + values: + description: + - On all entries matching the conditions in O(find), set the keys of this option to the values specified here. + - Use YAML V(~), or prepend keys with V(!), to specify to unset a value. + required: true + type: dict + require_matches_min: + description: + - Make sure that there are no less matches than this number. + - If there are less matches, fail instead of modifying anything. + type: int + default: 0 + require_matches_max: + description: + - Make sure that there are no more matches than this number. + - If there are more matches, fail instead of modifying anything. + - If not specified, there is no upper limit. + type: int + allow_no_matches: + description: + - Whether to allow that no match is found. + - If not specified, this value is induced from whether O(require_matches_min) is 0 or larger. + type: bool + ignore_dynamic: + description: + - Whether to ignore dynamic entries. + - By default, they are considered. If set to V(true), they are not considered. + - It is generally recommended to set this to V(true) unless when you really need to modify dynamic entries. + type: bool + default: false + version_added: 3.7.0 + ignore_builtin: + description: + - Whether to ignore builtin entries. + - By default, they are considered. If set to V(true), they are not considered. + - It is generally recommended to set this to V(true) unless when you really need to modify builtin entries. + type: bool + default: false + version_added: 3.7.0 +seealso: + - module: community.routeros.api + - module: community.routeros.api_facts + - module: community.routeros.api_modify + - module: community.routeros.api_info +""" + +EXAMPLES = r""" +--- +- name: Rename bridge from 'bridge' to 'my-bridge' + community.routeros.api_find_and_modify: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: interface bridge + find: + name: bridge + values: + name: my-bridge + # Always ignore dynamic and builtin entries + # (not relevant for this path, but generally recommended) + ignore_dynamic: true + ignore_builtin: true + +- name: Change IP address to 192.168.1.1 for interface bridge - assuming there is only one + community.routeros.api_find_and_modify: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: ip address + find: + interface: bridge + values: + address: "192.168.1.1/24" + # If there are zero entries, or more than one: fail! We expected that + # exactly one is configured. + require_matches_min: 1 + require_matches_max: 1 + # Always ignore dynamic and builtin entries + # (not relevant for this path, but generally recommended) + ignore_dynamic: true + ignore_builtin: true +""" + +RETURN = r""" +old_data: + description: + - A list of all elements for the current path before a change was made. + sample: + - '.id': '*1' + actual-interface: bridge + address: "192.168.88.1/24" + comment: defconf + disabled: false + dynamic: false + interface: bridge + invalid: false + network: 192.168.88.0 + type: list + elements: dict + returned: success +new_data: + description: + - A list of all elements for the current path after a change was made. + sample: + - '.id': '*1' + actual-interface: bridge + address: "192.168.1.1/24" + comment: awesome + disabled: false + dynamic: false + interface: bridge + invalid: false + network: 192.168.1.0 + type: list + elements: dict + returned: success +match_count: + description: + - The number of entries that matched the criteria in O(find). + sample: 1 + type: int + returned: success +modify__count: + description: + - The number of entries that were modified. + sample: 1 + type: int + returned: success +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.routeros.plugins.module_utils.api import ( + api_argument_spec, + check_has_library, + create_api, +) + +from ansible_collections.community.routeros.plugins.module_utils._api_data import ( + split_path, +) + +try: + from librouteros.exceptions import LibRouterosError +except Exception: + # Handled in api module_utils + pass + + +def compose_api_path(api, path): + api_path = api.path() + for p in path: + api_path = api_path.join(p) + return api_path + + +def filter_entries(entries, ignore_dynamic=False, ignore_builtin=False): + result = [] + for entry in entries: + if ignore_dynamic and entry.get('dynamic', False): + continue + if ignore_builtin and entry.get('builtin', False): + continue + result.append(entry) + return result + + +DISABLED_MEANS_EMPTY_STRING = ('comment', ) + + +def main(): + module_args = dict( + path=dict(type='str', required=True), + find=dict(type='dict', required=True), + values=dict(type='dict', required=True), + require_matches_min=dict(type='int', default=0), + require_matches_max=dict(type='int'), + allow_no_matches=dict(type='bool'), + ignore_dynamic=dict(type='bool', default=False), + ignore_builtin=dict(type='bool', default=False), + ) + module_args.update(api_argument_spec()) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + if module.params['allow_no_matches'] is None: + module.params['allow_no_matches'] = module.params['require_matches_min'] <= 0 + + find = module.params['find'] + for key, value in sorted(find.items()): + if key.startswith('!'): + key = key[1:] + if value not in (None, ''): + module.fail_json(msg='The value for "!{key}" in `find` must not be non-trivial!'.format(key=key)) + if key in find: + module.fail_json(msg='`find` must not contain both "{key}" and "!{key}"!'.format(key=key)) + values = module.params['values'] + for key, value in sorted(values.items()): + if key.startswith('!'): + key = key[1:] + if value not in (None, ''): + module.fail_json(msg='The value for "!{key}" in `values` must not be non-trivial!'.format(key=key)) + if key in values: + module.fail_json(msg='`values` must not contain both "{key}" and "!{key}"!'.format(key=key)) + + ignore_dynamic = module.params['ignore_dynamic'] + ignore_builtin = module.params['ignore_builtin'] + + check_has_library(module) + api = create_api(module) + + path = split_path(module.params['path']) + + api_path = compose_api_path(api, path) + + old_data = filter_entries(list(api_path), ignore_dynamic=ignore_dynamic, ignore_builtin=ignore_builtin) + new_data = [entry.copy() for entry in old_data] + + # Find matching entries + matching_entries = [] + for index, entry in enumerate(new_data): + matches = True + for key, value in find.items(): + if key.startswith('!'): + # Allow to specify keys that should not be present by prepending '!' + key = key[1:] + value = None + current_value = entry.get(key) + if key in DISABLED_MEANS_EMPTY_STRING and value == '' and current_value is None: + current_value = value + if current_value != value: + matches = False + break + if matches: + matching_entries.append((index, entry)) + + # Check whether the correct amount of entries was found + if matching_entries: + if len(matching_entries) < module.params['require_matches_min']: + module.fail_json(msg='Found %d entries, but expected at least %d' % (len(matching_entries), module.params['require_matches_min'])) + if module.params['require_matches_max'] is not None and len(matching_entries) > module.params['require_matches_max']: + module.fail_json(msg='Found %d entries, but expected at most %d' % (len(matching_entries), module.params['require_matches_max'])) + elif not module.params['allow_no_matches']: + module.fail_json(msg='Found no entries, but allow_no_matches=false') + + # Identify entries to update + modifications = [] + for index, entry in matching_entries: + modification = {} + for key, value in values.items(): + if key.startswith('!'): + # Allow to specify keys to remove by prepending '!' + key = key[1:] + value = None + current_value = entry.get(key) + if key in DISABLED_MEANS_EMPTY_STRING and value == '' and current_value is None: + current_value = value + if current_value != value: + if value is None: + disable_key = '!%s' % key + if key in DISABLED_MEANS_EMPTY_STRING: + disable_key = key + modification[disable_key] = '' + entry.pop(key, None) + else: + modification[key] = value + entry[key] = value + if modification: + if '.id' in entry: + modification['.id'] = entry['.id'] + modifications.append(modification) + + # Apply changes + if not module.check_mode and modifications: + for modification in modifications: + try: + api_path.update(**modification) + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while modifying for .id={id}: {error}'.format( + id=modification['.id'], + error=to_native(e), + ) + ) + new_data = filter_entries(list(api_path), ignore_dynamic=ignore_dynamic, ignore_builtin=ignore_builtin) + + # Produce return value + more = {} + if module._diff: + # Only include the matching values + more['diff'] = { + 'before': { + 'values': [old_data[index] for index, entry in matching_entries], + }, + 'after': { + 'values': [entry for index, entry in matching_entries], + }, + } + module.exit_json( + changed=bool(modifications), + old_data=old_data, + new_data=new_data, + match_count=len(matching_entries), + modify_count=len(modifications), + **more + ) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/api_info.py b/plugins/modules/api_info.py new file mode 100644 index 0000000..1eb8a94 --- /dev/null +++ b/plugins/modules/api_info.py @@ -0,0 +1,500 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r""" +module: api_info +author: + - "Felix Fontein (@felixfontein)" +short_description: Retrieve information from API +version_added: 2.2.0 +description: + - Allows to retrieve information for a path using the API. + - This can be used to backup a path to restore it with the M(community.routeros.api_modify) module. + - Entries are normalized, dynamic and builtin entries are not returned. Use the O(handle_disabled) and O(hide_defaults) + options to control normalization, the O(include_dynamic) and O(include_builtin) options to also return dynamic resp. builtin + entries, and use O(unfiltered) to return all fields including counters. + - B(Note) that this module is still heavily in development, and only supports B(some) paths. If you want to support new + paths, or think you found problems with existing paths, please first L(create an issue in the community.routeros Issue + Tracker,https://github.com/ansible-collections/community.routeros/issues/). +extends_documentation_fragment: + - community.routeros.api + - community.routeros.api.restrict + - community.routeros.attributes + - community.routeros.attributes.actiongroup_api + - community.routeros.attributes.idempotent_not_modify_state + - community.routeros.attributes.info_module +attributes: + platform: + support: full + platforms: RouterOS +options: + path: + description: + - Path to query. + - An example value is V(ip address). This is equivalent to running C(/ip address print) in the RouterOS CLI. + required: true + type: str + choices: + # BEGIN PATH LIST + - caps-man aaa + - caps-man access-list + - caps-man channel + - caps-man configuration + - caps-man datapath + - caps-man manager + - caps-man manager interface + - caps-man provisioning + - caps-man security + - certificate settings + - interface 6to4 + - interface bonding + - interface bridge + - interface bridge mlag + - interface bridge port + - interface bridge port-controller + - interface bridge port-extender + - interface bridge settings + - interface bridge vlan + - interface detect-internet + - interface eoip + - interface ethernet + - interface ethernet poe + - interface ethernet switch + - interface ethernet switch port + - interface ethernet switch port-isolation + - interface gre + - interface gre6 + - interface l2tp-client + - interface l2tp-server server + - interface list + - interface list member + - interface ovpn-client + - interface ovpn-server server + - interface ppp-client + - interface pppoe-client + - interface pppoe-server server + - interface pptp-server server + - interface sstp-server server + - interface vlan + - interface vrrp + - interface wifi + - interface wifi aaa + - interface wifi access-list + - interface wifi cap + - interface wifi capsman + - interface wifi channel + - interface wifi configuration + - interface wifi datapath + - interface wifi interworking + - interface wifi provisioning + - interface wifi security + - interface wifi steering + - interface wifiwave2 + - interface wifiwave2 aaa + - interface wifiwave2 access-list + - interface wifiwave2 cap + - interface wifiwave2 capsman + - interface wifiwave2 channel + - interface wifiwave2 configuration + - interface wifiwave2 datapath + - interface wifiwave2 interworking + - interface wifiwave2 provisioning + - interface wifiwave2 security + - interface wifiwave2 steering + - interface wireguard + - interface wireguard peers + - interface wireless + - interface wireless access-list + - interface wireless align + - interface wireless cap + - interface wireless connect-list + - interface wireless security-profiles + - interface wireless sniffer + - interface wireless snooper + - iot modbus + - ip accounting + - ip accounting web-access + - ip address + - ip arp + - ip cloud + - ip cloud advanced + - ip dhcp-client + - ip dhcp-client option + - ip dhcp-relay + - ip dhcp-server + - ip dhcp-server config + - ip dhcp-server lease + - ip dhcp-server matcher + - ip dhcp-server network + - ip dhcp-server option + - ip dhcp-server option sets + - ip dns + - ip dns adlist + - ip dns forwarders + - ip dns static + - ip firewall address-list + - ip firewall connection tracking + - ip firewall filter + - ip firewall layer7-protocol + - ip firewall mangle + - ip firewall nat + - ip firewall raw + - ip firewall service-port + - ip hotspot service-port + - ip ipsec identity + - ip ipsec mode-config + - ip ipsec peer + - ip ipsec policy + - ip ipsec profile + - ip ipsec proposal + - ip ipsec settings + - ip neighbor discovery-settings + - ip pool + - ip proxy + - ip route + - ip route rule + - ip route vrf + - ip service + - ip settings + - ip smb + - ip socks + - ip ssh + - ip tftp settings + - ip traffic-flow + - ip traffic-flow ipfix + - ip traffic-flow target + - ip upnp + - ip upnp interfaces + - ip vrf + - ipv6 address + - ipv6 dhcp-client + - ipv6 dhcp-server + - ipv6 dhcp-server option + - ipv6 firewall address-list + - ipv6 firewall filter + - ipv6 firewall mangle + - ipv6 firewall nat + - ipv6 firewall raw + - ipv6 nd + - ipv6 nd prefix + - ipv6 nd prefix default + - ipv6 route + - ipv6 settings + - mpls + - mpls interface + - mpls ldp + - mpls ldp accept-filter + - mpls ldp advertise-filter + - mpls ldp interface + - port firmware + - port remote-access + - ppp aaa + - ppp profile + - ppp secret + - queue interface + - queue simple + - queue tree + - queue type + - radius + - radius incoming + - routing bfd configuration + - routing bgp aggregate + - routing bgp connection + - routing bgp instance + - routing bgp network + - routing bgp peer + - routing bgp template + - routing filter + - routing filter community-list + - routing filter num-list + - routing filter rule + - routing filter select-rule + - routing id + - routing igmp-proxy + - routing igmp-proxy interface + - routing mme + - routing ospf area + - routing ospf area range + - routing ospf instance + - routing ospf interface-template + - routing ospf static-neighbor + - routing pimsm instance + - routing pimsm interface-template + - routing rip + - routing ripng + - routing rule + - routing table + - snmp + - snmp community + - system clock + - system clock manual + - system health settings + - system identity + - system leds settings + - system logging + - system logging action + - system note + - system ntp client + - system ntp client servers + - system ntp server + - system package update + - system resource irq rps + - system routerboard settings + - system scheduler + - system script + - system upgrade mirror + - system ups + - system watchdog + - tool bandwidth-server + - tool e-mail + - tool graphing + - tool graphing interface + - tool graphing resource + - tool mac-server + - tool mac-server mac-winbox + - tool mac-server ping + - tool netwatch + - tool romon + - tool sms + - tool sniffer + - tool traffic-generator + - user + - user aaa + - user group + - user settings + # END PATH LIST + unfiltered: + description: + - Whether to output all fields, and not just the ones supported as input for M(community.routeros.api_modify). + - Unfiltered output can contain counters and other state information. + type: bool + default: false + handle_disabled: + description: + - How to handle unset values. + - V(exclamation) prepends the keys with V(!) in the output with value V(null). + - V(null-value) uses the regular key with value V(null). + - V(omit) omits these values from the result. + type: str + choices: + - exclamation + - null-value + - omit + default: exclamation + hide_defaults: + description: + - Whether to hide default values. + type: bool + default: true + include_dynamic: + description: + - Whether to include dynamic values. + - By default, they are not returned, and the C(dynamic) keys are omitted. + - If set to V(true), they are returned as well, and the C(dynamic) keys are returned as well. + type: bool + default: false + include_builtin: + description: + - Whether to include builtin values. + - By default, they are not returned, and the C(builtin) keys are omitted. + - If set to V(true), they are returned as well, and the C(builtin) keys are returned as well. + type: bool + default: false + version_added: 2.4.0 + include_read_only: + description: + - Whether to include read-only fields. + - By default, they are not returned. + type: bool + default: false + version_added: 2.10.0 + restrict: + description: + - Restrict output to entries matching the following criteria. + version_added: 2.18.0 +seealso: + - module: community.routeros.api + - module: community.routeros.api_facts + - module: community.routeros.api_find_and_modify + - module: community.routeros.api_modify +""" + +EXAMPLES = r""" +--- +- name: Get IP addresses + community.routeros.api_info: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: ip address + register: ip_addresses + +- name: Print data for IP addresses + ansible.builtin.debug: + var: ip_addresses.result + +- name: Get IP addresses + community.routeros.api_info: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: ip address + register: ip_addresses + +- name: Print data for IP addresses + ansible.builtin.debug: + var: ip_addresses.result +""" + +RETURN = r""" +result: + description: A list of all elements for the current path. + sample: + - '.id': '*1' + actual-interface: bridge + address: "192.168.88.1/24" + comment: defconf + disabled: false + dynamic: false + interface: bridge + invalid: false + network: 192.168.88.0 + type: list + elements: dict + returned: always +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.routeros.plugins.module_utils.api import ( + api_argument_spec, + check_has_library, + create_api, + get_api_version, +) + +from ansible_collections.community.routeros.plugins.module_utils._api_data import ( + PATHS, + join_path, + split_path, +) + +from ansible_collections.community.routeros.plugins.module_utils._api_helper import ( + restrict_argument_spec, + restrict_entry_accepted, + validate_and_prepare_restrict, +) + +try: + from librouteros.exceptions import LibRouterosError +except Exception: + # Handled in api module_utils + pass + + +def compose_api_path(api, path): + api_path = api.path() + for p in path: + api_path = api_path.join(p) + return api_path + + +def main(): + module_args = dict( + path=dict(type='str', required=True, choices=sorted([join_path(path) for path in PATHS if PATHS[path].fully_understood])), + unfiltered=dict(type='bool', default=False), + handle_disabled=dict(type='str', choices=['exclamation', 'null-value', 'omit'], default='exclamation'), + hide_defaults=dict(type='bool', default=True), + include_dynamic=dict(type='bool', default=False), + include_builtin=dict(type='bool', default=False), + include_read_only=dict(type='bool', default=False), + ) + module_args.update(api_argument_spec()) + module_args.update(restrict_argument_spec()) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + + check_has_library(module) + api = create_api(module) + + path = split_path(module.params['path']) + versioned_path_info = PATHS.get(tuple(path)) + if versioned_path_info is None: + module.fail_json(msg='Path /{path} is not yet supported'.format(path='/'.join(path))) + if versioned_path_info.needs_version: + api_version = get_api_version(api) + supported, not_supported_msg = versioned_path_info.provide_version(api_version) + if not supported: + msg = 'Path /{path} is not supported for API version {api_version}'.format(path='/'.join(path), api_version=api_version) + if not_supported_msg: + msg = '{0}: {1}'.format(msg, not_supported_msg) + module.fail_json(msg=msg) + path_info = versioned_path_info.get_data() + + handle_disabled = module.params['handle_disabled'] + hide_defaults = module.params['hide_defaults'] + include_dynamic = module.params['include_dynamic'] + include_builtin = module.params['include_builtin'] + include_read_only = module.params['include_read_only'] + restrict_data = validate_and_prepare_restrict(module, path_info) + try: + api_path = compose_api_path(api, path) + + result = [] + unfiltered = module.params['unfiltered'] + for entry in api_path: + if not include_dynamic: + if entry.get('dynamic', False): + continue + if not include_builtin: + if entry.get('builtin', False): + continue + if not restrict_entry_accepted(entry, path_info, restrict_data): + continue + if not unfiltered: + for k in list(entry): + if k == '.id': + continue + if k == 'dynamic' and include_dynamic: + continue + if k == 'builtin' and include_builtin: + continue + if k not in path_info.fields: + entry.pop(k) + if handle_disabled != 'omit': + for k, field_info in path_info.fields.items(): + if field_info.write_only: + entry.pop(k, None) + continue + if k not in entry: + if handle_disabled == 'exclamation': + k = '!%s' % k + entry[k] = None + for k, field_info in path_info.fields.items(): + if hide_defaults: + if field_info.default is not None and entry.get(k) == field_info.default: + entry.pop(k) + if field_info.absent_value and k not in entry: + entry[k] = field_info.absent_value + if not include_read_only and k in entry and field_info.read_only: + entry.pop(k) + result.append(entry) + + module.exit_json(result=result) + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json(msg=to_native(e)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/api_modify.py b/plugins/modules/api_modify.py new file mode 100644 index 0000000..dfa6fd5 --- /dev/null +++ b/plugins/modules/api_modify.py @@ -0,0 +1,1266 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r""" +module: api_modify +author: + - "Felix Fontein (@felixfontein)" +short_description: Modify data at paths with API +version_added: 2.2.0 +description: + - Allows to modify information for a path using the API. + - Use the M(community.routeros.api_find_and_modify) module to modify one or multiple entries in a controlled way depending + on some search conditions. + - To make a backup of a path that can be restored with this module, use the M(community.routeros.api_info) module. + - The module ignores dynamic and builtin entries. + - B(Note) that this module is still heavily in development, and only supports B(some) paths. If you want to support new + paths, or think you found problems with existing paths, please first L(create an issue in the community.routeros Issue + Tracker,https://github.com/ansible-collections/community.routeros/issues/). +notes: + - If write-only fields are present in the path, the module is B(not idempotent) in a strict sense, since it is not able + to verify the current value of these fields. The behavior the module should assume can be controlled with the O(handle_write_only) + option. +requirements: + - Needs L(ordereddict,https://pypi.org/project/ordereddict) for Python 2.6 +extends_documentation_fragment: + - community.routeros.api + - community.routeros.api.restrict + - community.routeros.attributes + - community.routeros.attributes.actiongroup_api +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + support: full + platforms: RouterOS + idempotent: + support: full +options: + path: + description: + - Path to query. + - An example value is V(ip address). This is equivalent to running modification commands in C(/ip address) in the RouterOS + CLI. + required: true + type: str + choices: + # BEGIN PATH LIST + - caps-man aaa + - caps-man access-list + - caps-man channel + - caps-man configuration + - caps-man datapath + - caps-man manager + - caps-man manager interface + - caps-man provisioning + - caps-man security + - certificate settings + - interface 6to4 + - interface bonding + - interface bridge + - interface bridge mlag + - interface bridge port + - interface bridge port-controller + - interface bridge port-extender + - interface bridge settings + - interface bridge vlan + - interface detect-internet + - interface eoip + - interface ethernet + - interface ethernet poe + - interface ethernet switch + - interface ethernet switch port + - interface ethernet switch port-isolation + - interface gre + - interface gre6 + - interface l2tp-client + - interface l2tp-server server + - interface list + - interface list member + - interface ovpn-client + - interface ovpn-server server + - interface ppp-client + - interface pppoe-client + - interface pppoe-server server + - interface pptp-server server + - interface sstp-server server + - interface vlan + - interface vrrp + - interface wifi + - interface wifi aaa + - interface wifi access-list + - interface wifi cap + - interface wifi capsman + - interface wifi channel + - interface wifi configuration + - interface wifi datapath + - interface wifi interworking + - interface wifi provisioning + - interface wifi security + - interface wifi steering + - interface wifiwave2 + - interface wifiwave2 aaa + - interface wifiwave2 access-list + - interface wifiwave2 cap + - interface wifiwave2 capsman + - interface wifiwave2 channel + - interface wifiwave2 configuration + - interface wifiwave2 datapath + - interface wifiwave2 interworking + - interface wifiwave2 provisioning + - interface wifiwave2 security + - interface wifiwave2 steering + - interface wireguard + - interface wireguard peers + - interface wireless + - interface wireless access-list + - interface wireless align + - interface wireless cap + - interface wireless connect-list + - interface wireless security-profiles + - interface wireless sniffer + - interface wireless snooper + - iot modbus + - ip accounting + - ip accounting web-access + - ip address + - ip arp + - ip cloud + - ip cloud advanced + - ip dhcp-client + - ip dhcp-client option + - ip dhcp-relay + - ip dhcp-server + - ip dhcp-server config + - ip dhcp-server lease + - ip dhcp-server matcher + - ip dhcp-server network + - ip dhcp-server option + - ip dhcp-server option sets + - ip dns + - ip dns adlist + - ip dns forwarders + - ip dns static + - ip firewall address-list + - ip firewall connection tracking + - ip firewall filter + - ip firewall layer7-protocol + - ip firewall mangle + - ip firewall nat + - ip firewall raw + - ip firewall service-port + - ip hotspot service-port + - ip ipsec identity + - ip ipsec mode-config + - ip ipsec peer + - ip ipsec policy + - ip ipsec profile + - ip ipsec proposal + - ip ipsec settings + - ip neighbor discovery-settings + - ip pool + - ip proxy + - ip route + - ip route rule + - ip route vrf + - ip service + - ip settings + - ip smb + - ip socks + - ip ssh + - ip tftp settings + - ip traffic-flow + - ip traffic-flow ipfix + - ip traffic-flow target + - ip upnp + - ip upnp interfaces + - ip vrf + - ipv6 address + - ipv6 dhcp-client + - ipv6 dhcp-server + - ipv6 dhcp-server option + - ipv6 firewall address-list + - ipv6 firewall filter + - ipv6 firewall mangle + - ipv6 firewall nat + - ipv6 firewall raw + - ipv6 nd + - ipv6 nd prefix + - ipv6 nd prefix default + - ipv6 route + - ipv6 settings + - mpls + - mpls interface + - mpls ldp + - mpls ldp accept-filter + - mpls ldp advertise-filter + - mpls ldp interface + - port firmware + - port remote-access + - ppp aaa + - ppp profile + - ppp secret + - queue interface + - queue simple + - queue tree + - queue type + - radius + - radius incoming + - routing bfd configuration + - routing bgp aggregate + - routing bgp connection + - routing bgp instance + - routing bgp network + - routing bgp peer + - routing bgp template + - routing filter + - routing filter community-list + - routing filter num-list + - routing filter rule + - routing filter select-rule + - routing id + - routing igmp-proxy + - routing igmp-proxy interface + - routing mme + - routing ospf area + - routing ospf area range + - routing ospf instance + - routing ospf interface-template + - routing ospf static-neighbor + - routing pimsm instance + - routing pimsm interface-template + - routing rip + - routing ripng + - routing rule + - routing table + - snmp + - snmp community + - system clock + - system clock manual + - system health settings + - system identity + - system leds settings + - system logging + - system logging action + - system note + - system ntp client + - system ntp client servers + - system ntp server + - system package update + - system resource irq rps + - system routerboard settings + - system scheduler + - system script + - system upgrade mirror + - system ups + - system watchdog + - tool bandwidth-server + - tool e-mail + - tool graphing + - tool graphing interface + - tool graphing resource + - tool mac-server + - tool mac-server mac-winbox + - tool mac-server ping + - tool netwatch + - tool romon + - tool sms + - tool sniffer + - tool traffic-generator + - user + - user aaa + - user group + - user settings + # END PATH LIST + data: + description: + - Data to ensure that is present for this path. + - Fields not provided will not be modified. + - If C(.id) appears in an entry, it will be ignored. + required: true + type: list + elements: dict + ensure_order: + description: + - Whether to ensure the same order of the config as present in O(data). + - Requires O(handle_absent_entries=remove). + type: bool + default: false + handle_absent_entries: + description: + - How to handle entries that are present in the current config, but not in O(data). + - V(ignore) ignores them. + - V(remove) removes them. + type: str + choices: + - ignore + - remove + default: ignore + handle_entries_content: + description: + - For a single entry in O(data), this describes how to handle fields that are not mentioned in that entry, but appear + in the actual config. + - If V(ignore), they are not modified. + - If V(remove), they are removed. If at least one cannot be removed, the module will fail. + - If V(remove_as_much_as_possible), all that can be removed will be removed. The ones that cannot be removed will be + kept. + - Note that V(remove) and V(remove_as_much_as_possible) do not apply to write-only fields. + type: str + choices: + - ignore + - remove + - remove_as_much_as_possible + default: ignore + handle_read_only: + description: + - How to handle values passed in for read-only fields. + - If V(ignore), they are not passed to the API. + - If V(validate), the values are not passed for creation, and for updating they are compared to the value returned for + the object. If they differ, the module fails. + - If V(error), the module will fail if read-only fields are provided. + type: str + choices: + - ignore + - validate + - error + default: error + version_added: 2.10.0 + handle_write_only: + description: + - How to handle values passed in for write-only fields. + - If V(create_only), they are passed on creation, and ignored for updating. + - If V(always_update), they are always passed to the API. This means that if such a value is present, the module will + always result in C(changed) since there is no way to validate whether the value actually changed. + - If V(error), the module will fail if write-only fields are provided. + type: str + choices: + - create_only + - always_update + - error + default: create_only + version_added: 2.10.0 + restrict: + description: + - Restrict operation to entries matching the following criteria. + - This can be useful together with O(handle_absent_entries=remove) to operate on a subset of the values. + - For example, for O(path=ip firewall filter), you can set O(restrict[].field=chain) and O(restrict[].values=input) + to restrict operation to the input chain, and ignore the forward and output chains. + version_added: 2.18.0 +seealso: + - module: community.routeros.api + - module: community.routeros.api_facts + - module: community.routeros.api_find_and_modify + - module: community.routeros.api_info +""" + +EXAMPLES = r""" +--- +- name: Setup DHCP server networks + # Ensures that we have exactly two DHCP server networks (in the specified order) + community.routeros.api_modify: + path: ip dhcp-server network + handle_absent_entries: remove + handle_entries_content: remove_as_much_as_possible + ensure_order: true + data: + - address: 192.168.88.0/24 + comment: admin network + dns-server: 192.168.88.1 + gateway: 192.168.88.1 + - address: 192.168.1.0/24 + comment: customer network 1 + dns-server: 192.168.1.1 + gateway: 192.168.1.1 + netmask: 24 + +- name: Adjust NAT + community.routeros.api_modify: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: ip firewall nat + data: + - action: masquerade + chain: srcnat + comment: NAT to WAN + out-interface-list: WAN + # Three ways to unset values: + # - nothing after `:` + # - "empty" value (null/~/None) + # - prepend '!' + out-interface: + to-addresses: ~ + '!to-ports': + +- name: Block all incoming connections + community.routeros.api_modify: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: ip firewall filter + handle_absent_entries: remove + handle_entries_content: remove_as_much_as_possible + restrict: + # Do not touch any chain except the input chain + - field: chain + values: + - input + data: + - action: drop + chain: input +""" + +RETURN = r""" +old_data: + description: + - A list of all elements for the current path before a change was made. + sample: + - '.id': '*1' + actual-interface: bridge + address: "192.168.88.1/24" + comment: defconf + disabled: false + dynamic: false + interface: bridge + invalid: false + network: 192.168.88.0 + type: list + elements: dict + returned: always +new_data: + description: + - A list of all elements for the current path after a change was made. + sample: + - '.id': '*1' + actual-interface: bridge + address: "192.168.1.1/24" + comment: awesome + disabled: false + dynamic: false + interface: bridge + invalid: false + network: 192.168.1.0 + type: list + elements: dict + returned: always +""" + +from collections import defaultdict + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.routeros.plugins.module_utils.api import ( + api_argument_spec, + check_has_library, + create_api, + get_api_version, +) + +from ansible_collections.community.routeros.plugins.module_utils._api_data import ( + PATHS, + join_path, + split_path, +) + +from ansible_collections.community.routeros.plugins.module_utils._api_helper import ( + restrict_argument_spec, + restrict_entry_accepted, + validate_and_prepare_restrict, +) + +HAS_ORDEREDDICT = True +try: + from collections import OrderedDict +except ImportError: + try: + from ordereddict import OrderedDict + except ImportError: + HAS_ORDEREDDICT = False + OrderedDict = dict + +try: + from librouteros.exceptions import LibRouterosError +except Exception: + # Handled in api module_utils + pass + + +def compose_api_path(api, path): + api_path = api.path() + for p in path: + api_path = api_path.join(p) + return api_path + + +def find_modifications(old_entry, new_entry, path_info, module, for_text='', return_none_instead_of_fail=False): + modifications = OrderedDict() + updated_entry = old_entry.copy() + for k, v in new_entry.items(): + if k == '.id': + continue + disabled_k = None + if k.startswith('!'): + disabled_k = k[1:] + elif v is None or v == path_info.fields[k].remove_value: + disabled_k = k + if disabled_k is not None: + if disabled_k in old_entry: + if path_info.fields[disabled_k].remove_value is not None: + modifications[disabled_k] = path_info.fields[disabled_k].remove_value + else: + modifications['!%s' % disabled_k] = '' + del updated_entry[disabled_k] + continue + if k not in old_entry and path_info.fields[k].default == v and not path_info.fields[k].can_disable: + continue + key_info = path_info.fields[k] + if key_info.read_only: + # handle_read_only must be 'validate' + if old_entry.get(k) != v: + module.fail_json( + msg='Read-only key "{key}" has value "{old_value}", but should have new value "{new_value}"{for_text}.'.format( + key=k, old_value=old_entry.get(k), new_value=v, for_text=for_text)) + continue + if key_info.write_only: + if module.params['handle_write_only'] == 'create_only': + # do not update this value + continue + if k not in old_entry or old_entry[k] != v: + modifications[k] = v + updated_entry[k] = v + handle_entries_content = module.params['handle_entries_content'] + if handle_entries_content != 'ignore': + for k in old_entry: + if k == '.id' or k in new_entry or ('!%s' % k) in new_entry or k not in path_info.fields: + continue + field_info = path_info.fields[k] + if field_info.default is not None and field_info.default == old_entry[k]: + continue + if field_info.remove_value is not None and field_info.remove_value == old_entry[k]: + continue + if field_info.can_disable: + if field_info.default is not None: + modifications[k] = field_info.default + elif field_info.remove_value is not None: + modifications[k] = field_info.remove_value + else: + modifications['!%s' % k] = '' + del updated_entry[k] + elif field_info.default is not None: + modifications[k] = field_info.default + updated_entry[k] = field_info.default + elif handle_entries_content == 'remove': + if return_none_instead_of_fail: + return None, None + module.fail_json(msg='Key "{key}" cannot be removed{for_text}.'.format(key=k, for_text=for_text)) + for k in path_info.fields: + field_info = path_info.fields[k] + if k not in old_entry and k not in new_entry and field_info.can_disable and field_info.default is not None: + modifications[k] = field_info.default + updated_entry[k] = field_info.default + return modifications, updated_entry + + +def essentially_same_weight(old_entry, new_entry, path_info, module): + for k, v in new_entry.items(): + if k == '.id': + continue + disabled_k = None + if k.startswith('!'): + disabled_k = k[1:] + elif v is None or v == path_info.fields[k].remove_value: + disabled_k = k + if disabled_k is not None: + if disabled_k in old_entry: + return None + continue + if k not in old_entry and path_info.fields[k].default == v: + continue + if k not in old_entry or old_entry[k] != v: + return None + handle_entries_content = module.params['handle_entries_content'] + weight = 0 + for k in old_entry: + if k == '.id' or k in new_entry or ('!%s' % k) in new_entry or k not in path_info.fields: + continue + field_info = path_info.fields[k] + if field_info.default is not None and field_info.default == old_entry[k]: + continue + if handle_entries_content != 'ignore': + return None + else: + weight += 1 + return weight + + +def remove_read_only(entry, path_info): + to_remove = [] + for real_k, v in entry.items(): + k = real_k + if k.startswith('!'): + k = k[1:] + if path_info.fields[k].read_only: + to_remove.append(real_k) + for k in to_remove: + entry.pop(k) + + +def format_pk(primary_keys, values): + return ', '.join('{pk}="{value}"'.format(pk=pk, value=value) for pk, value in zip(primary_keys, values)) + + +def polish_entry(entry, path_info, module, for_text): + if '.id' in entry: + entry.pop('.id') + to_remove = [] + for key, value in entry.items(): + real_key = key + disabled_key = False + if key.startswith('!'): + disabled_key = True + key = key[1:] + if key in entry: + module.fail_json(msg='Not both "{key}" and "!{key}" must appear{for_text}.'.format(key=key, for_text=for_text)) + key_info = path_info.fields.get(key) + if key_info is None: + module.fail_json(msg='Unknown key "{key}"{for_text}.'.format(key=real_key, for_text=for_text)) + if disabled_key: + if not key_info.can_disable: + module.fail_json(msg='Key "!{key}" must not be disabled (leading "!"){for_text}.'.format(key=key, for_text=for_text)) + if value not in (None, '', key_info.remove_value): + module.fail_json(msg='Disabled key "!{key}" must not have a value{for_text}.'.format(key=key, for_text=for_text)) + elif value is None: + if not key_info.can_disable: + module.fail_json(msg='Key "{key}" must not be disabled (value null/~/None){for_text}.'.format(key=key, for_text=for_text)) + if key_info.read_only: + if module.params['handle_read_only'] == 'error': + module.fail_json(msg='Key "{key}" is read-only{for_text}, and handle_read_only=error.'.format(key=key, for_text=for_text)) + if module.params['handle_read_only'] == 'ignore': + to_remove.append(real_key) + if key_info.write_only: + if module.params['handle_write_only'] == 'error': + module.fail_json(msg='Key "{key}" is write-only{for_text}, and handle_write_only=error.'.format(key=key, for_text=for_text)) + for key in to_remove: + entry.pop(key) + for key, field_info in path_info.fields.items(): + if field_info.required and key not in entry: + module.fail_json(msg='Key "{key}" must be present{for_text}.'.format(key=key, for_text=for_text)) + for require_list in path_info.required_one_of: + found_req_keys = [rk for rk in require_list if rk in entry] + if len(require_list) > 0 and not found_req_keys: + module.fail_json( + msg='Every element in data must contain one of {required_keys}. For example, the element{for_text} does not provide it.'.format( + required_keys=', '.join(['"{k}"'.format(k=k) for k in require_list]), + for_text=for_text, + ) + ) + for exclusive_list in path_info.mutually_exclusive: + found_ex_keys = [ek for ek in exclusive_list if ek in entry] + if len(found_ex_keys) > 1: + module.fail_json( + msg='Keys {exclusive_keys} cannot be used at the same time{for_text}.'.format( + exclusive_keys=', '.join(['"{k}"'.format(k=k) for k in found_ex_keys]), + for_text=for_text, + ) + ) + + +def remove_irrelevant_data(entry, path_info): + for k, v in list(entry.items()): + if k == '.id': + continue + if k not in path_info.fields or v is None: + del entry[k] + + +def match_entries(new_entries, old_entries, path_info, module): + matching_old_entries = [None for entry in new_entries] + old_entries = list(old_entries) + matches = [] + handle_absent_entries = module.params['handle_absent_entries'] + if handle_absent_entries == 'remove': + for new_index, (unused, new_entry) in enumerate(new_entries): + for old_index, (unused, old_entry) in enumerate(old_entries): + modifications, unused = find_modifications(old_entry, new_entry, path_info, module, return_none_instead_of_fail=True) + if modifications is not None: + matches.append((new_index, old_index, len(modifications))) + else: + for new_index, (unused, new_entry) in enumerate(new_entries): + for old_index, (unused, old_entry) in enumerate(old_entries): + weight = essentially_same_weight(old_entry, new_entry, path_info, module) + if weight is not None: + matches.append((new_index, old_index, weight)) + matches.sort(key=lambda entry: entry[2]) + for new_index, old_index, rating in matches: + if matching_old_entries[new_index] is not None or old_entries[old_index] is None: + continue + matching_old_entries[new_index], old_entries[old_index] = old_entries[old_index], None + unmatched_old_entries = [index_entry for index_entry in old_entries if index_entry is not None] + return matching_old_entries, unmatched_old_entries + + +def remove_dynamic(entries): + result = [] + for entry in entries: + if entry.get('dynamic', False) or entry.get('builtin', False): + continue + result.append(entry) + return result + + +def get_api_data(api_path, path_info): + entries = list(api_path) + for entry in entries: + for k, field_info in path_info.fields.items(): + if field_info.absent_value is not None and k not in entry: + entry[k] = field_info.absent_value + return entries + + +def prepare_for_add(entry, path_info): + new_entry = {} + for k, v in entry.items(): + if k.startswith('!'): + real_k = k[1:] + remove_value = path_info.fields[real_k].remove_value + if remove_value is not None: + k = real_k + v = remove_value + else: + if v is None: + v = path_info.fields[k].remove_value + new_entry[k] = v + return new_entry + + +def remove_rejected(data, path_info, restrict_data): + return [ + entry for entry in data + if restrict_entry_accepted(entry, path_info, restrict_data) + ] + + +def sync_list(module, api, path, path_info, restrict_data): + handle_absent_entries = module.params['handle_absent_entries'] + handle_entries_content = module.params['handle_entries_content'] + if handle_absent_entries == 'remove': + if handle_entries_content == 'ignore': + module.fail_json( + msg='For this path, handle_absent_entries=remove cannot be combined with handle_entries_content=ignore' + ) + + stratify_keys = path_info.stratify_keys or () + + data = module.params['data'] + stratified_data = defaultdict(list) + for index, entry in enumerate(data): + if not restrict_entry_accepted(entry, path_info, restrict_data): + module.fail_json(msg='The element at index #{index} does not match `restrict`'.format(index=index + 1)) + for stratify_key in stratify_keys: + if stratify_key not in entry: + module.fail_json( + msg='Every element in data must contain "{stratify_key}". For example, the element at index #{index} does not provide it.'.format( + stratify_key=stratify_key, + index=index + 1, + ) + ) + sks = tuple(entry[stratify_key] for stratify_key in stratify_keys) + polish_entry( + entry, path_info, module, + ' at index {index}'.format(index=index + 1), + ) + stratified_data[sks].append((index, entry)) + stratified_data = dict(stratified_data) + + api_path = compose_api_path(api, path) + + old_data = get_api_data(api_path, path_info) + old_data = remove_dynamic(old_data) + old_data = remove_rejected(old_data, path_info, restrict_data) + stratified_old_data = defaultdict(list) + for index, entry in enumerate(old_data): + sks = tuple(entry[stratify_key] for stratify_key in stratify_keys) + stratified_old_data[sks].append((index, entry)) + stratified_old_data = dict(stratified_old_data) + + create_list = [] + modify_list = [] + remove_list = [] + + new_data = [] + for key, indexed_entries in stratified_data.items(): + old_entries = stratified_old_data.pop(key, []) + + # Try to match indexed_entries with old_entries + matching_old_entries, unmatched_old_entries = match_entries(indexed_entries, old_entries, path_info, module) + + # Update existing entries + for (index, new_entry), potential_old_entry in zip(indexed_entries, matching_old_entries): + if potential_old_entry is not None: + old_index, old_entry = potential_old_entry + modifications, updated_entry = find_modifications( + old_entry, new_entry, path_info, module, + ' at index {index}'.format(index=index + 1), + ) + # Add to modification list if there are changes + if modifications: + modifications['.id'] = old_entry['.id'] + modify_list.append(modifications) + new_data.append((old_index, updated_entry)) + new_entry['.id'] = old_entry['.id'] + else: + remove_read_only(new_entry, path_info) + create_list.append(new_entry) + + if handle_absent_entries == 'remove': + remove_list.extend(entry['.id'] for index, entry in unmatched_old_entries) + else: + new_data.extend(unmatched_old_entries) + + for key, entries in stratified_old_data.items(): + if handle_absent_entries == 'remove': + remove_list.extend(entry['.id'] for index, entry in entries) + else: + new_data.extend(entries) + + new_data = [entry for index, entry in sorted(new_data, key=lambda entry: entry[0])] + new_data.extend(create_list) + + reorder_list = [] + if module.params['ensure_order']: + for index, entry in enumerate(data): + if '.id' in entry: + def match(current_entry): + return current_entry['.id'] == entry['.id'] + + else: + def match(current_entry): + return current_entry is entry + + current_index = next(current_index + index for current_index, current_entry in enumerate(new_data[index:]) if match(current_entry)) + if current_index != index: + reorder_list.append((index, new_data[current_index], new_data[index])) + new_data.insert(index, new_data.pop(current_index)) + + if not module.check_mode: + if remove_list: + try: + api_path.remove(*remove_list) + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while removing {remove_list}: {error}'.format( + remove_list=', '.join(['ID {id}'.format(id=id) for id in remove_list]), + error=to_native(e), + ) + ) + for modifications in modify_list: + try: + api_path.update(**modifications) + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while modifying for ID {id}: {error}'.format( + id=modifications['.id'], + error=to_native(e), + ) + ) + for entry in create_list: + try: + entry['.id'] = api_path.add(**prepare_for_add(entry, path_info)) + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while creating entry: {error}'.format( + error=to_native(e), + ) + ) + for new_index, new_entry, old_entry in reorder_list: + try: + for res in api_path('move', numbers=new_entry['.id'], destination=old_entry['.id']): + pass + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while moving entry ID {element_id} to position #{new_index} ID ({new_id}): {error}'.format( + element_id=new_entry['.id'], + new_index=new_index, + new_id=old_entry['.id'], + error=to_native(e), + ) + ) + + # For sake of completeness, retrieve the full new data: + if modify_list or create_list or reorder_list: + new_data = remove_dynamic(get_api_data(api_path, path_info)) + new_data = remove_rejected(new_data, path_info, restrict_data) + + # Remove 'irrelevant' data + for entry in old_data: + remove_irrelevant_data(entry, path_info) + for entry in new_data: + remove_irrelevant_data(entry, path_info) + + # Produce return value + more = {} + if module._diff: + more['diff'] = { + 'before': { + 'data': old_data, + }, + 'after': { + 'data': new_data, + }, + } + module.exit_json( + changed=bool(create_list or modify_list or remove_list or reorder_list), + old_data=old_data, + new_data=new_data, + **more + ) + + +def sync_with_primary_keys(module, api, path, path_info, restrict_data): + primary_keys = path_info.primary_keys + + if path_info.fixed_entries: + if module.params['ensure_order']: + module.fail_json(msg='ensure_order=true cannot be used with this path') + if module.params['handle_absent_entries'] == 'remove': + module.fail_json(msg='handle_absent_entries=remove cannot be used with this path') + + data = module.params['data'] + new_data_by_key = OrderedDict() + for index, entry in enumerate(data): + if not restrict_entry_accepted(entry, path_info, restrict_data): + module.fail_json(msg='The element at index #{index} does not match `restrict`'.format(index=index + 1)) + for primary_key in primary_keys: + if primary_key not in entry: + module.fail_json( + msg='Every element in data must contain "{primary_key}". For example, the element at index #{index} does not provide it.'.format( + primary_key=primary_key, + index=index + 1, + ) + ) + pks = tuple(entry[primary_key] for primary_key in primary_keys) + if pks in new_data_by_key: + module.fail_json( + msg='Every element in data must contain a unique value for {primary_keys}. The value {value} appears at least twice.'.format( + primary_keys=','.join(primary_keys), + value=','.join(['"{0}"'.format(pk) for pk in pks]), + ) + ) + polish_entry( + entry, path_info, module, + ' for {values}'.format( + values=', '.join([ + '{primary_key}="{value}"'.format(primary_key=primary_key, value=value) + for primary_key, value in zip(primary_keys, pks) + ]) + ), + ) + new_data_by_key[pks] = entry + + api_path = compose_api_path(api, path) + + old_data = get_api_data(api_path, path_info) + old_data = remove_dynamic(old_data) + old_data = remove_rejected(old_data, path_info, restrict_data) + old_data_by_key = OrderedDict() + id_by_key = {} + for entry in old_data: + pks = tuple(entry[primary_key] for primary_key in primary_keys) + old_data_by_key[pks] = entry + id_by_key[pks] = entry['.id'] + new_data = [] + + create_list = [] + modify_list = [] + remove_list = [] + remove_keys = [] + handle_absent_entries = module.params['handle_absent_entries'] + for key, old_entry in old_data_by_key.items(): + new_entry = new_data_by_key.pop(key, None) + if new_entry is None: + if handle_absent_entries == 'remove': + remove_list.append(old_entry['.id']) + remove_keys.append(key) + else: + new_data.append(old_entry) + else: + modifications, updated_entry = find_modifications( + old_entry, new_entry, path_info, module, + ' for {values}'.format( + values=', '.join([ + '{primary_key}="{value}"'.format(primary_key=primary_key, value=value) + for primary_key, value in zip(primary_keys, key) + ]) + ) + ) + new_data.append(updated_entry) + # Add to modification list if there are changes + if modifications: + modifications['.id'] = old_entry['.id'] + modify_list.append((key, modifications)) + for new_entry in new_data_by_key.values(): + if path_info.fixed_entries: + module.fail_json(msg='Cannot add new entry {values} to this path'.format( + values=', '.join([ + '{primary_key}="{value}"'.format(primary_key=primary_key, value=new_entry[primary_key]) + for primary_key in primary_keys + ]), + )) + remove_read_only(new_entry, path_info) + create_list.append(new_entry) + new_entry = new_entry.copy() + for key in list(new_entry): + if key.startswith('!'): + new_entry.pop(key) + new_data.append(new_entry) + + reorder_list = [] + if module.params['ensure_order']: + index_by_key = dict() + for index, entry in enumerate(new_data): + index_by_key[tuple(entry[primary_key] for primary_key in primary_keys)] = index + for index, source_entry in enumerate(data): + source_pks = tuple(source_entry[primary_key] for primary_key in primary_keys) + source_index = index_by_key.pop(source_pks) + if index == source_index: + continue + entry = new_data[index] + pks = tuple(entry[primary_key] for primary_key in primary_keys) + reorder_list.append((source_pks, index, pks)) + for k, v in index_by_key.items(): + if v >= index and v < source_index: + index_by_key[k] = v + 1 + new_data.insert(index, new_data.pop(source_index)) + + if not module.check_mode: + if remove_list: + try: + api_path.remove(*remove_list) + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while removing {remove_list}: {error}'.format( + remove_list=', '.join([ + '{identifier} (ID {id})'.format(identifier=format_pk(primary_keys, key), id=id) + for id, key in zip(remove_list, remove_keys) + ]), + error=to_native(e), + ) + ) + for key, modifications in modify_list: + try: + api_path.update(**modifications) + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while modifying for {identifier} (ID {id}): {error}'.format( + identifier=format_pk(primary_keys, key), + id=modifications['.id'], + error=to_native(e), + ) + ) + for entry in create_list: + try: + entry['.id'] = api_path.add(**prepare_for_add(entry, path_info)) + # Store ID for primary keys + pks = tuple(entry[primary_key] for primary_key in primary_keys) + id_by_key[pks] = entry['.id'] + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while creating entry for {identifier}: {error}'.format( + identifier=format_pk(primary_keys, [entry[pk] for pk in primary_keys]), + error=to_native(e), + ) + ) + for element_pks, new_index, new_pks in reorder_list: + try: + element_id = id_by_key[element_pks] + new_id = id_by_key[new_pks] + for res in api_path('move', numbers=element_id, destination=new_id): + pass + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json( + msg='Error while moving entry ID {element_id} to position of ID {new_id}: {error}'.format( + element_id=element_id, + new_id=new_id, + error=to_native(e), + ) + ) + + # For sake of completeness, retrieve the full new data: + if modify_list or create_list or reorder_list: + new_data = remove_dynamic(get_api_data(api_path, path_info)) + new_data = remove_rejected(new_data, path_info, restrict_data) + + # Remove 'irrelevant' data + for entry in old_data: + remove_irrelevant_data(entry, path_info) + for entry in new_data: + remove_irrelevant_data(entry, path_info) + + # Produce return value + more = {} + if module._diff: + more['diff'] = { + 'before': { + 'data': old_data, + }, + 'after': { + 'data': new_data, + }, + } + module.exit_json( + changed=bool(create_list or modify_list or remove_list or reorder_list), + old_data=old_data, + new_data=new_data, + **more + ) + + +def sync_single_value(module, api, path, path_info, restrict_data): + if module.params['restrict'] is not None: + module.fail_json(msg='The restrict option cannot be used with this path, since there is precisely one entry.') + data = module.params['data'] + if len(data) != 1: + module.fail_json(msg='Data must be a list with exactly one element.') + new_entry = data[0] + polish_entry(new_entry, path_info, module, '') + + api_path = compose_api_path(api, path) + + old_data = get_api_data(api_path, path_info) + if len(old_data) != 1: + module.fail_json( + msg='Internal error: retrieving /{path} resulted in {count} elements. Expected exactly 1.'.format( + path=join_path(path), + count=len(old_data) + ) + ) + old_entry = old_data[0] + + # Determine modifications + modifications, updated_entry = find_modifications(old_entry, new_entry, path_info, module, '') + # Do modifications + if modifications: + if not module.check_mode: + # Actually do modification + try: + api_path.update(**modifications) + except (LibRouterosError, UnicodeEncodeError) as e: + module.fail_json(msg='Error while modifying: {error}'.format(error=to_native(e))) + # Retrieve latest version + new_data = get_api_data(api_path, path_info) + if len(new_data) == 1: + updated_entry = new_data[0] + + # Remove 'irrelevant' data + remove_irrelevant_data(old_entry, path_info) + remove_irrelevant_data(updated_entry, path_info) + + # Produce return value + more = {} + if module._diff: + more['diff'] = { + 'before': old_entry, + 'after': updated_entry, + } + module.exit_json( + changed=bool(modifications), + old_data=[old_entry], + new_data=[updated_entry], + **more + ) + + +def get_backend(path_info): + if path_info is None: + return None + if not path_info.fully_understood: + return None + + if path_info.primary_keys: + return sync_with_primary_keys + + if path_info.single_value: + return sync_single_value + + if not path_info.has_identifier: + return sync_list + + return None + + +def has_backend(versioned_path_info): + if not versioned_path_info.fully_understood: + return False + + if versioned_path_info.unversioned is not None: + return get_backend(versioned_path_info.unversioned) is not None + + if versioned_path_info.versioned is not None: + for dummy, dummy, unversioned in versioned_path_info.versioned: + if unversioned and not isinstance(unversioned, str) and get_backend(unversioned) is not None: + return True + + return False + + +def main(): + path_choices = sorted([join_path(path) for path, versioned_path_info in PATHS.items() if has_backend(versioned_path_info)]) + module_args = dict( + path=dict(type='str', required=True, choices=path_choices), + data=dict(type='list', elements='dict', required=True), + handle_absent_entries=dict(type='str', choices=['ignore', 'remove'], default='ignore'), + handle_entries_content=dict(type='str', choices=['ignore', 'remove', 'remove_as_much_as_possible'], default='ignore'), + ensure_order=dict(type='bool', default=False), + handle_read_only=dict(type='str', default='error', choices=['ignore', 'validate', 'error']), + handle_write_only=dict(type='str', default='create_only', choices=['create_only', 'always_update', 'error']), + ) + module_args.update(api_argument_spec()) + module_args.update(restrict_argument_spec()) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + if module.params['ensure_order'] and module.params['handle_absent_entries'] == 'ignore': + module.fail_json(msg='ensure_order=true requires handle_absent_entries=remove') + + if not HAS_ORDEREDDICT: + # This should never happen for Python 2.7+ + module.fail_json(msg=missing_required_lib('ordereddict')) + + check_has_library(module) + api = create_api(module) + + path = split_path(module.params['path']) + versioned_path_info = PATHS.get(tuple(path)) + if versioned_path_info.needs_version: + api_version = get_api_version(api) + supported, not_supported_msg = versioned_path_info.provide_version(api_version) + if not supported: + msg = 'Path /{path} is not supported for API version {api_version}'.format(path='/'.join(path), api_version=api_version) + if not_supported_msg: + msg = '{0}: {1}'.format(msg, not_supported_msg) + module.fail_json(msg=msg) + path_info = versioned_path_info.get_data() + + backend = get_backend(path_info) + if path_info is None or backend is None: + module.fail_json(msg='Path /{path} is not yet supported'.format(path='/'.join(path))) + + restrict_data = validate_and_prepare_restrict(module, path_info) + + backend(module, api, path, path_info, restrict_data) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/command.py b/plugins/modules/command.py index 82331a5..50abe49 100644 --- a/plugins/modules/command.py +++ b/plugins/modules/command.py @@ -1,63 +1,83 @@ #!/usr/bin/python -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) __metaclass__ = type -DOCUMENTATION = ''' ---- +DOCUMENTATION = r""" module: command author: "Egor Zaitsev (@heuels)" short_description: Run commands on remote devices running MikroTik RouterOS description: - - Sends arbitrary commands to an RouterOS node and returns the results - read from the device. This module includes an - argument that will cause the module to wait for a specific condition - before returning or timing out if the condition is not met. + - Sends arbitrary commands to an RouterOS node and returns the results read from the device. This module includes an argument + that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. + - The module always indicates a (changed) status. You can use R(the changed_when task property,override_the_changed_result) + to determine whether a command task actually resulted in a change or not. +extends_documentation_fragment: + - community.routeros.attributes +attributes: + check_mode: + support: none + details: + - Before community.routeros 3.0.0, the module claimed to support check mode. It simply executed the command in check + mode. + diff_mode: + support: none + platform: + support: full + platforms: RouterOS + idempotent: + support: N/A + details: + - Whether the executed command is idempotent depends on the command. options: commands: description: - - List of commands to send to the remote RouterOS device over the - configured provider. The resulting output from the command - is returned. If the I(wait_for) argument is provided, the - module is not returned until the condition is satisfied or - the number of retries has expired. + - List of commands to send to the remote RouterOS device over the configured provider. The resulting output from the + command is returned. If the O(wait_for) argument is provided, the module is not returned until the condition is satisfied + or the number of retries has expired. required: true + type: list + elements: str wait_for: description: - - List of conditions to evaluate against the output of the - command. The task will wait for each condition to be true - before moving forward. If the conditional is not true - within the configured number of retries, the task fails. - See examples. + - List of conditions to evaluate against the output of the command. The task will wait for each condition to be true + before moving forward. If the conditional is not true within the configured number of retries, the task fails. See + examples. + type: list + elements: str match: description: - - The I(match) argument is used in conjunction with the - I(wait_for) argument to specify the match policy. Valid - values are C(all) or C(any). If the value is set to C(all) - then all conditionals in the wait_for must be satisfied. If - the value is set to C(any) then only one of the values must be - satisfied. + - The O(match) argument is used in conjunction with the O(wait_for) argument to specify the match policy. Valid values + are V(all) or V(any). If the value is set to V(all) then all conditionals in the wait_for must be satisfied. If the + value is set to V(any) then only one of the values must be satisfied. default: all choices: ['any', 'all'] + type: str retries: description: - - Specifies the number of retries a command should by tried - before it is considered failed. The command is run on the - target device every retry and evaluated against the - I(wait_for) conditions. + - Specifies the number of retries a command should by tried before it is considered failed. The command is run on the + target device every retry and evaluated against the O(wait_for) conditions. default: 10 + type: int interval: description: - - Configures the interval in seconds to wait between retries - of the command. If the command does not pass the specified - conditions, the interval indicates how long to wait before - trying the command again. + - Configures the interval in seconds to wait between retries of the command. If the command does not pass the specified + conditions, the interval indicates how long to wait before trying the command again. default: 1 -''' + type: int +seealso: + - ref: ansible_collections.community.routeros.docsite.ssh-guide + description: How to connect to RouterOS devices with SSH. + - ref: ansible_collections.community.routeros.docsite.quoting + description: How to quote and unquote commands and arguments. +""" -EXAMPLES = """ +EXAMPLES = r""" +--- - name: Run command on remote devices community.routeros.command: commands: /system routerboard print @@ -83,31 +103,29 @@ EXAMPLES = """ - result[1] contains ether1 """ -RETURN = """ +RETURN = r""" stdout: - description: The set of responses from the commands + description: The set of responses from the commands. returned: always apart from low level errors (such as action plugin) type: list sample: ['...', '...'] stdout_lines: - description: The value of stdout split into a list + description: The value of stdout split into a list. returned: always apart from low level errors (such as action plugin) type: list sample: [['...', '...'], ['...'], ['...']] failed_conditions: - description: The list of conditionals that have failed + description: The list of conditionals that have failed. returned: failed type: list sample: ['...', '...'] """ -import re import time from ansible_collections.community.routeros.plugins.module_utils.routeros import run_commands from ansible_collections.community.routeros.plugins.module_utils.routeros import routeros_argument_spec from ansible.module_utils.basic import AnsibleModule -from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ComplexList from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import Conditional from ansible.module_utils.six import string_types @@ -123,10 +141,10 @@ def main(): """main entry point for module execution """ argument_spec = dict( - commands=dict(type='list', required=True), + commands=dict(type='list', elements='str', required=True), - wait_for=dict(type='list'), - match=dict(default='all', choices=['all', 'any']), + wait_for=dict(type='list', elements='str'), + match=dict(type='str', default='all', choices=['all', 'any']), retries=dict(default=10, type='int'), interval=dict(default=1, type='int') @@ -135,7 +153,7 @@ def main(): argument_spec.update(routeros_argument_spec) module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) + supports_check_mode=False) result = {'changed': False} @@ -168,7 +186,7 @@ def main(): module.fail_json(msg=msg, failed_conditions=failed_conditions) result.update({ - 'changed': False, + 'changed': True, 'stdout': responses, 'stdout_lines': list(to_lines(responses)) }) diff --git a/plugins/modules/facts.py b/plugins/modules/facts.py index 6b37f23..e80143c 100644 --- a/plugins/modules/facts.py +++ b/plugins/modules/facts.py @@ -1,35 +1,48 @@ #!/usr/bin/python -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) __metaclass__ = type -DOCUMENTATION = ''' ---- +DOCUMENTATION = r""" module: facts author: "Egor Zaitsev (@heuels)" short_description: Collect facts from remote devices running MikroTik RouterOS description: - - Collects a base set of device facts from a remote device that - is running RotuerOS. This module prepends all of the - base network fact keys with C(ansible_net_). The facts - module will always collect a base set of facts from the device + - Collects a base set of device facts from a remote device that is running RouterOS. This module prepends all of the base + network fact keys with C(ansible_net_). The facts module will always collect a base set of facts from the device and can enable or disable collection of additional facts. +extends_documentation_fragment: + - community.routeros.attributes + - community.routeros.attributes.facts + - community.routeros.attributes.facts_module + - community.routeros.attributes.idempotent_not_modify_state +attributes: + platform: + support: full + platforms: RouterOS options: gather_subset: description: - - When supplied, this argument will restrict the facts collected - to a given subset. Possible values for this argument include - C(all), C(hardware), C(config), and C(interfaces). Can specify a list of - values to include a larger subset. Values can also be used - with an initial C(!) to specify that a specific subset should - not be collected. + - When supplied, this argument will restrict the facts collected to a given subset. Possible values for this argument + include V(all), V(hardware), V(config), V(interfaces), and V(routing). + - Can specify a list of values to include a larger subset. Values can also be used with an initial V(!) to specify that + a specific subset should not be collected. required: false - default: '!config' -''' + default: + - '!config' + type: list + elements: str +seealso: + - ref: ansible_collections.community.routeros.docsite.ssh-guide + description: How to connect to RouterOS devices with SSH. +""" -EXAMPLES = """ +EXAMPLES = r""" +--- - name: Collect all facts from the device community.routeros.facts: gather_subset: all @@ -45,123 +58,123 @@ EXAMPLES = """ - "!hardware" """ -RETURN = """ +RETURN = r""" ansible_facts: - description: "Dictionary of ip geolocation facts for a host's IP address" + description: "Dictionary of IP geolocation facts for a host's IP address." returned: always type: dict contains: ansible_net_gather_subset: - description: The list of fact subsets collected from the device + description: The list of fact subsets collected from the device. returned: always type: list # default ansible_net_model: - description: The model name returned from the device - returned: always + description: The model name returned from the device. + returned: O(gather_subset) contains V(default) type: str ansible_net_serialnum: - description: The serial number of the remote device - returned: always + description: The serial number of the remote device. + returned: O(gather_subset) contains V(default) type: str ansible_net_version: - description: The operating system version running on the remote device - returned: always + description: The operating system version running on the remote device. + returned: O(gather_subset) contains V(default) type: str ansible_net_hostname: - description: The configured hostname of the device - returned: always + description: The configured hostname of the device. + returned: O(gather_subset) contains V(default) type: str ansible_net_arch: - description: The CPU architecture of the device - returned: always + description: The CPU architecture of the device. + returned: O(gather_subset) contains V(default) type: str ansible_net_uptime: - description: The uptime of the device - returned: always + description: The uptime of the device. + returned: O(gather_subset) contains V(default) type: str ansible_net_cpu_load: - description: Current CPU load - returned: always + description: Current CPU load. + returned: O(gather_subset) contains V(default) type: str # hardware ansible_net_spacefree_mb: - description: The available disk space on the remote device in MiB - returned: when hardware is configured + description: The available disk space on the remote device in MiB. + returned: O(gather_subset) contains V(hardware) type: dict ansible_net_spacetotal_mb: - description: The total disk space on the remote device in MiB - returned: when hardware is configured + description: The total disk space on the remote device in MiB. + returned: O(gather_subset) contains V(hardware) type: dict ansible_net_memfree_mb: - description: The available free memory on the remote device in MiB - returned: when hardware is configured + description: The available free memory on the remote device in MiB. + returned: O(gather_subset) contains V(hardware) type: int ansible_net_memtotal_mb: - description: The total memory on the remote device in MiB - returned: when hardware is configured + description: The total memory on the remote device in MiB. + returned: O(gather_subset) contains V(hardware) type: int # config ansible_net_config: - description: The current active config from the device - returned: when config is configured + description: The current active config from the device. + returned: O(gather_subset) contains V(config) type: str ansible_net_config_nonverbose: description: - The current active config from the device in minimal form. - - This value is idempotent in the sense that if the facts module is run twice and the device's config - was not changed between the runs, the value is identical. This is achieved by running C(/export) - and stripping the timestamp from the comment in the first line. - returned: when config is configured + - This value is idempotent in the sense that if the facts module is run twice and the device's config was not changed + between the runs, the value is identical. This is achieved by running C(/export) and stripping the timestamp from + the comment in the first line. + returned: O(gather_subset) contains V(config) type: str version_added: 1.2.0 # interfaces ansible_net_all_ipv4_addresses: - description: All IPv4 addresses configured on the device - returned: when interfaces is configured + description: All IPv4 addresses configured on the device. + returned: O(gather_subset) contains V(interfaces) type: list ansible_net_all_ipv6_addresses: - description: All IPv6 addresses configured on the device - returned: when interfaces is configured + description: All IPv6 addresses configured on the device. + returned: O(gather_subset) contains V(interfaces) type: list ansible_net_interfaces: - description: A hash of all interfaces running on the system - returned: when interfaces is configured + description: A hash of all interfaces running on the system. + returned: O(gather_subset) contains V(interfaces) type: dict ansible_net_neighbors: - description: The list of neighbors from the remote device - returned: when interfaces is configured + description: The list of neighbors from the remote device. + returned: O(gather_subset) contains V(interfaces) type: dict # routing ansible_net_bgp_peer: - description: The dict bgp peer - returned: peer information + description: A dictionary with BGP peer information. + returned: O(gather_subset) contains V(routing) type: dict ansible_net_bgp_vpnv4_route: - description: The dict bgp vpnv4 route - returned: vpnv4 route information + description: A dictionary with BGP vpnv4 route information. + returned: O(gather_subset) contains V(routing) type: dict ansible_net_bgp_instance: - description: The dict bgp instance - returned: bgp instance information + description: A dictionary with BGP instance information. + returned: O(gather_subset) contains V(routing) type: dict ansible_net_route: - description: The dict routes in all routing table - returned: routes information in all routing table + description: A dictionary for routes in all routing tables. + returned: O(gather_subset) contains V(routing) type: dict ansible_net_ospf_instance: - description: The dict ospf instance - returned: ospf instance information + description: A dictionary with OSPF instances. + returned: O(gather_subset) contains V(routing) type: dict ansible_net_ospf_neighbor: - description: The dict ospf neighbor - returned: ospf neighbor information + description: A dictionary with OSPF neighbors. + returned: O(gather_subset) contains V(routing) type: dict """ import re @@ -295,7 +308,7 @@ class Config(FactsBase): '/export', ] - RM_DATE_RE = re.compile(r'^# [a-z0-9/][a-z0-9/]* [0-9:]* by RouterOS') + RM_DATE_RE = re.compile(r'^# [a-z0-9/-][a-z0-9/-]* [0-9:]* by RouterOS') def populate(self): super(Config, self).populate() @@ -375,7 +388,7 @@ class Interfaces(FactsBase): for line in data.split('\n'): if len(line) == 0 or line[:5] == 'Flags': continue - elif not re.match(self.WRAPPED_LINE_RE, line): + elif not preprocessed or not re.match(self.WRAPPED_LINE_RE, line): preprocessed.append(line) else: preprocessed[-1] += line @@ -452,7 +465,7 @@ class Routing(FactsBase): for line in data.split('\n'): if len(line) == 0 or line[:5] == 'Flags': continue - elif not re.match(self.WRAPPED_LINE_RE, line): + elif not preprocessed or not re.match(self.WRAPPED_LINE_RE, line): preprocessed.append(line) else: preprocessed[-1] += line @@ -576,14 +589,12 @@ FACT_SUBSETS = dict( VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) -warnings = list() - def main(): """main entry point for module execution """ argument_spec = dict( - gather_subset=dict(default=['!config'], type='list') + gather_subset=dict(default=['!config'], type='list', elements='str') ) argument_spec.update(routeros_argument_spec) @@ -640,7 +651,7 @@ def main(): key = 'ansible_net_%s' % key ansible_facts[key] = value - module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + module.exit_json(ansible_facts=ansible_facts) if __name__ == '__main__': diff --git a/plugins/terminal/routeros.py b/plugins/terminal/routeros.py index 8648ff5..8a39561 100644 --- a/plugins/terminal/routeros.py +++ b/plugins/terminal/routeros.py @@ -1,29 +1,13 @@ -# -# (c) 2016 Red Hat Inc. -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . -# +# Copyright (c) 2016 Red Hat Inc. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json import re from ansible.errors import AnsibleConnectionFailure -from ansible.module_utils.common.text.converters import to_text, to_bytes from ansible.plugins.terminal import TerminalBase from ansible.utils.display import Display @@ -47,7 +31,9 @@ class TerminalModule(TerminalBase): terminal_stdout_re = [ re.compile(br"\x1b<"), - re.compile(br"\[[\w\-\.]+\@[\w\s\-\.\/]+\] ?> ?$"), + re.compile( + br"((\[[\w\-\.]+\@)|(\r\<(([\w\-\.]*\@)|)))" + br"[\w\s\-\.\/]+\] ?( ?$"), re.compile(br"Please press \"Enter\" to continue!"), re.compile(br"Do you want to see the software license\? \[Y\/n\]: ?"), ] diff --git a/tests/config.yml b/tests/config.yml index ba0238e..38590f2 100644 --- a/tests/config.yml +++ b/tests/config.yml @@ -1,4 +1,8 @@ --- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + # See template for more information: # https://github.com/ansible/ansible/blob/devel/test/lib/ansible_test/config/config.yml modules: diff --git a/tests/ee/all.yml b/tests/ee/all.yml new file mode 100644 index 0000000..26f198b --- /dev/null +++ b/tests/ee/all.yml @@ -0,0 +1,18 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- hosts: localhost + tasks: + - name: Find all roles + find: + paths: + - "{{ (playbook_dir | default('.')) ~ '/roles' }}" + file_type: directory + depth: 1 + register: result + - name: Include all roles + include_role: + name: "{{ item }}" + loop: "{{ result.files | map(attribute='path') | map('regex_replace', '.*/', '') | sort }}" diff --git a/tests/ee/roles/filter_quoting/aliases b/tests/ee/roles/filter_quoting/aliases new file mode 100644 index 0000000..ddba818 --- /dev/null +++ b/tests/ee/roles/filter_quoting/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +shippable/posix/group1 +skip/python2.6 diff --git a/tests/ee/roles/filter_quoting/tasks/main.yml b/tests/ee/roles/filter_quoting/tasks/main.yml new file mode 100644 index 0000000..c80af59 --- /dev/null +++ b/tests/ee/roles/filter_quoting/tasks/main.yml @@ -0,0 +1,63 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "Test split filter" + assert: + that: + - "'' | community.routeros.split == []" + - "'foo bar' | community.routeros.split == ['foo', 'bar']" + - > + 'foo bar="a b c"' | community.routeros.split == ['foo', 'bar=a b c'] + +- name: "Test split filter error handling" + set_fact: + test: >- + {{ 'a="' | community.routeros.split }} + ignore_errors: true + register: result + +- name: "Verify split filter error handling" + assert: + that: + - >- + "Unexpected end of string during escaped parameter" in result.msg + +- name: "Test quote_argument filter" + assert: + that: + - > + 'a=' | community.routeros.quote_argument == 'a=""' + - > + 'a=b' | community.routeros.quote_argument == 'a=b' + - > + 'a=b c' | community.routeros.quote_argument == 'a="b\\_c"' + - > + 'a=""' | community.routeros.quote_argument == 'a="\\"\\""' + +- name: "Test quote_argument_value filter" + assert: + that: + - > + '' | community.routeros.quote_argument_value == '""' + - > + 'foo' | community.routeros.quote_argument_value == 'foo' + - > + '"foo bar"' | community.routeros.quote_argument_value == '"\\"foo\\_bar\\""' + +- name: "Test join filter" + assert: + that: + - > + ['a=', 'b=c d'] | community.routeros.join == 'a="" b="c\\_d"' + +- name: "Test list_to_dict filter" + assert: + that: + - > + ['a=', 'b=c'] | community.routeros.list_to_dict == {'a': '', 'b': 'c'} + - > + ['a=', 'b=c'] | community.routeros.list_to_dict(skip_empty_values=True) == {'b': 'c'} + - > + ['a', 'b=c'] | community.routeros.list_to_dict(require_assignment=False) == {'a': none, 'b': 'c'} diff --git a/tests/ee/roles/smoke/tasks/main.yml b/tests/ee/roles/smoke/tasks/main.yml new file mode 100644 index 0000000..b992c8e --- /dev/null +++ b/tests/ee/roles/smoke/tasks/main.yml @@ -0,0 +1,43 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Run api module + community.routeros.api: + username: foo + password: bar + hostname: localhost + path: ip address + ignore_errors: true + register: result + +- name: Validate result + assert: + that: + - result is failed + - result.msg in potential_errors + vars: + potential_errors: + - "Error while connecting: [Errno 111] Connection refused" + - "Error while connecting: [Errno 99] Cannot assign requested address" + +- name: Run command module + community.routeros.command: + commands: + - /ip address print + vars: + ansible_host: localhost + ansible_connection: ansible.netcommon.network_cli + ansible_network_os: community.routeros.routeros + ansible_user: foo + ansible_ssh_pass: bar + ansible_ssh_port: 12349 + ignore_errors: true + register: result + +- name: Validate result + assert: + that: + - result is failed + - "'Unable to connect to port 12349 ' in result.msg or 'ssh connect failed: Connection refused' in result.msg" diff --git a/tests/integration/requirements.yml b/tests/integration/requirements.yml new file mode 100644 index 0000000..559c301 --- /dev/null +++ b/tests/integration/requirements.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +collections: + - ansible.netcommon diff --git a/tests/integration/targets/filter_quoting/aliases b/tests/integration/targets/filter_quoting/aliases new file mode 100644 index 0000000..ddba818 --- /dev/null +++ b/tests/integration/targets/filter_quoting/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +shippable/posix/group1 +skip/python2.6 diff --git a/tests/integration/targets/filter_quoting/tasks/main.yml b/tests/integration/targets/filter_quoting/tasks/main.yml new file mode 100644 index 0000000..c80af59 --- /dev/null +++ b/tests/integration/targets/filter_quoting/tasks/main.yml @@ -0,0 +1,63 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "Test split filter" + assert: + that: + - "'' | community.routeros.split == []" + - "'foo bar' | community.routeros.split == ['foo', 'bar']" + - > + 'foo bar="a b c"' | community.routeros.split == ['foo', 'bar=a b c'] + +- name: "Test split filter error handling" + set_fact: + test: >- + {{ 'a="' | community.routeros.split }} + ignore_errors: true + register: result + +- name: "Verify split filter error handling" + assert: + that: + - >- + "Unexpected end of string during escaped parameter" in result.msg + +- name: "Test quote_argument filter" + assert: + that: + - > + 'a=' | community.routeros.quote_argument == 'a=""' + - > + 'a=b' | community.routeros.quote_argument == 'a=b' + - > + 'a=b c' | community.routeros.quote_argument == 'a="b\\_c"' + - > + 'a=""' | community.routeros.quote_argument == 'a="\\"\\""' + +- name: "Test quote_argument_value filter" + assert: + that: + - > + '' | community.routeros.quote_argument_value == '""' + - > + 'foo' | community.routeros.quote_argument_value == 'foo' + - > + '"foo bar"' | community.routeros.quote_argument_value == '"\\"foo\\_bar\\""' + +- name: "Test join filter" + assert: + that: + - > + ['a=', 'b=c d'] | community.routeros.join == 'a="" b="c\\_d"' + +- name: "Test list_to_dict filter" + assert: + that: + - > + ['a=', 'b=c'] | community.routeros.list_to_dict == {'a': '', 'b': 'c'} + - > + ['a=', 'b=c'] | community.routeros.list_to_dict(skip_empty_values=True) == {'b': 'c'} + - > + ['a', 'b=c'] | community.routeros.list_to_dict(require_assignment=False) == {'a': none, 'b': 'c'} diff --git a/tests/requirements.yml b/tests/requirements.yml deleted file mode 100644 index a218740..0000000 --- a/tests/requirements.yml +++ /dev/null @@ -1,4 +0,0 @@ -integration_tests_dependencies: -- ansible.netcommon -unit_tests_dependencies: -- ansible.netcommon diff --git a/tests/sanity/extra/extra-docs.json b/tests/sanity/extra/extra-docs.json deleted file mode 100644 index a62ef37..0000000 --- a/tests/sanity/extra/extra-docs.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "include_symlinks": false, - "prefixes": [ - "docs/docsite/" - ], - "output": "path-line-column-message", - "requirements": [ - "antsibull" - ] -} diff --git a/tests/sanity/extra/extra-docs.py b/tests/sanity/extra/extra-docs.py deleted file mode 100755 index f4b7f59..0000000 --- a/tests/sanity/extra/extra-docs.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -"""Check extra collection docs with antsibull-lint.""" -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os -import sys -import subprocess - - -def main(): - """Main entry point.""" - if not os.path.isdir(os.path.join('docs', 'docsite')): - return - p = subprocess.run(['antsibull-lint', 'collection-docs', '.'], check=False) - if p.returncode not in (0, 3): - print('{0}:0:0: unexpected return code {1}'.format(sys.argv[0], p.returncode)) - - -if __name__ == '__main__': - main() diff --git a/tests/sanity/extra/no-unwanted-files.json b/tests/sanity/extra/no-unwanted-files.json deleted file mode 100644 index c789a7f..0000000 --- a/tests/sanity/extra/no-unwanted-files.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "include_symlinks": true, - "prefixes": [ - "plugins/" - ], - "output": "path-message" -} diff --git a/tests/sanity/extra/no-unwanted-files.py b/tests/sanity/extra/no-unwanted-files.py deleted file mode 100755 index 49806f2..0000000 --- a/tests/sanity/extra/no-unwanted-files.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -"""Prevent unwanted files from being added to the source tree.""" -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os -import sys - - -def main(): - """Main entry point.""" - paths = sys.argv[1:] or sys.stdin.read().splitlines() - - allowed_extensions = ( - '.cs', - '.ps1', - '.psm1', - '.py', - ) - - skip_paths = set([ - ]) - - skip_directories = ( - ) - - for path in paths: - if path in skip_paths: - continue - - if any(path.startswith(skip_directory) for skip_directory in skip_directories): - continue - - ext = os.path.splitext(path)[1] - - if ext not in allowed_extensions: - print('%s: extension must be one of: %s' % (path, ', '.join(allowed_extensions))) - - -if __name__ == '__main__': - main() diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 9d5934b..50c92fb 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -1,5 +1,9 @@ -plugins/modules/command.py validate-modules:doc-missing-type -plugins/modules/command.py validate-modules:parameter-list-no-elements -plugins/modules/command.py validate-modules:parameter-type-not-in-doc -plugins/modules/facts.py validate-modules:parameter-list-no-elements -plugins/modules/facts.py validate-modules:parameter-type-not-in-doc +docs/docsite/rst/api-guide.rst rstcheck +docs/docsite/rst/quoting.rst rstcheck +docs/docsite/rst/ssh-guide.rst rstcheck +tests/update-docs.py compile-2.6 +tests/update-docs.py compile-2.7 +tests/update-docs.py compile-3.5 +tests/update-docs.py future-import-boilerplate +tests/update-docs.py metaclass-boilerplate +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.10.txt.license b/tests/sanity/ignore-2.10.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.10.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 9d5934b..1c7f5da 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -1,5 +1,6 @@ -plugins/modules/command.py validate-modules:doc-missing-type -plugins/modules/command.py validate-modules:parameter-list-no-elements -plugins/modules/command.py validate-modules:parameter-type-not-in-doc -plugins/modules/facts.py validate-modules:parameter-list-no-elements -plugins/modules/facts.py validate-modules:parameter-type-not-in-doc +tests/update-docs.py compile-2.6 +tests/update-docs.py compile-2.7 +tests/update-docs.py compile-3.5 +tests/update-docs.py future-import-boilerplate +tests/update-docs.py metaclass-boilerplate +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.11.txt.license b/tests/sanity/ignore-2.11.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.11.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 9d5934b..65e5bca 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -1,5 +1 @@ -plugins/modules/command.py validate-modules:doc-missing-type -plugins/modules/command.py validate-modules:parameter-list-no-elements -plugins/modules/command.py validate-modules:parameter-type-not-in-doc -plugins/modules/facts.py validate-modules:parameter-list-no-elements -plugins/modules/facts.py validate-modules:parameter-type-not-in-doc +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.12.txt.license b/tests/sanity/ignore-2.12.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.12.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt new file mode 100644 index 0000000..65e5bca --- /dev/null +++ b/tests/sanity/ignore-2.13.txt @@ -0,0 +1 @@ +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.13.txt.license b/tests/sanity/ignore-2.13.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.13.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt new file mode 100644 index 0000000..65e5bca --- /dev/null +++ b/tests/sanity/ignore-2.14.txt @@ -0,0 +1 @@ +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.14.txt.license b/tests/sanity/ignore-2.14.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.14.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt new file mode 100644 index 0000000..65e5bca --- /dev/null +++ b/tests/sanity/ignore-2.15.txt @@ -0,0 +1 @@ +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.15.txt.license b/tests/sanity/ignore-2.15.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.15.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt new file mode 100644 index 0000000..65e5bca --- /dev/null +++ b/tests/sanity/ignore-2.16.txt @@ -0,0 +1 @@ +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.16.txt.license b/tests/sanity/ignore-2.16.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.16.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt new file mode 100644 index 0000000..65e5bca --- /dev/null +++ b/tests/sanity/ignore-2.17.txt @@ -0,0 +1 @@ +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.17.txt.license b/tests/sanity/ignore-2.17.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.17.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt new file mode 100644 index 0000000..65e5bca --- /dev/null +++ b/tests/sanity/ignore-2.18.txt @@ -0,0 +1 @@ +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.18.txt.license b/tests/sanity/ignore-2.18.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.18.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt new file mode 100644 index 0000000..65e5bca --- /dev/null +++ b/tests/sanity/ignore-2.19.txt @@ -0,0 +1 @@ +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.19.txt.license b/tests/sanity/ignore-2.19.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.19.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.20.txt b/tests/sanity/ignore-2.20.txt new file mode 100644 index 0000000..65e5bca --- /dev/null +++ b/tests/sanity/ignore-2.20.txt @@ -0,0 +1 @@ +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.20.txt.license b/tests/sanity/ignore-2.20.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.20.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index c1e40c7..50c92fb 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -1,3 +1,9 @@ -plugins/modules/command.py validate-modules:doc-missing-type -plugins/modules/command.py validate-modules:parameter-type-not-in-doc -plugins/modules/facts.py validate-modules:parameter-type-not-in-doc +docs/docsite/rst/api-guide.rst rstcheck +docs/docsite/rst/quoting.rst rstcheck +docs/docsite/rst/ssh-guide.rst rstcheck +tests/update-docs.py compile-2.6 +tests/update-docs.py compile-2.7 +tests/update-docs.py compile-3.5 +tests/update-docs.py future-import-boilerplate +tests/update-docs.py metaclass-boilerplate +tests/update-docs.py shebang diff --git a/tests/sanity/ignore-2.9.txt.license b/tests/sanity/ignore-2.9.txt.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/sanity/ignore-2.9.txt.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/compat/__init__.py b/tests/unit/compat/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/compat/builtins.py b/tests/unit/compat/builtins.py deleted file mode 100644 index f60ee67..0000000 --- a/tests/unit/compat/builtins.py +++ /dev/null @@ -1,33 +0,0 @@ -# (c) 2014, Toshio Kuratomi -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -# -# Compat for python2.7 -# - -# One unittest needs to import builtins via __import__() so we need to have -# the string that represents it -try: - import __builtin__ -except ImportError: - BUILTINS = 'builtins' -else: - BUILTINS = '__builtin__' diff --git a/tests/unit/compat/mock.py b/tests/unit/compat/mock.py deleted file mode 100644 index 0972cd2..0000000 --- a/tests/unit/compat/mock.py +++ /dev/null @@ -1,122 +0,0 @@ -# (c) 2014, Toshio Kuratomi -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -''' -Compat module for Python3.x's unittest.mock module -''' -import sys - -# Python 2.7 - -# Note: Could use the pypi mock library on python3.x as well as python2.x. It -# is the same as the python3 stdlib mock library - -try: - # Allow wildcard import because we really do want to import all of mock's - # symbols into this compat shim - # pylint: disable=wildcard-import,unused-wildcard-import - from unittest.mock import * -except ImportError: - # Python 2 - # pylint: disable=wildcard-import,unused-wildcard-import - try: - from mock import * - except ImportError: - print('You need the mock library installed on python2.x to run tests') - - -# Prior to 3.4.4, mock_open cannot handle binary read_data -if sys.version_info >= (3,) and sys.version_info < (3, 4, 4): - file_spec = None - - def _iterate_read_data(read_data): - # Helper for mock_open: - # Retrieve lines from read_data via a generator so that separate calls to - # readline, read, and readlines are properly interleaved - sep = b'\n' if isinstance(read_data, bytes) else '\n' - data_as_list = [l + sep for l in read_data.split(sep)] - - if data_as_list[-1] == sep: - # If the last line ended in a newline, the list comprehension will have an - # extra entry that's just a newline. Remove this. - data_as_list = data_as_list[:-1] - else: - # If there wasn't an extra newline by itself, then the file being - # emulated doesn't have a newline to end the last line remove the - # newline that our naive format() added - data_as_list[-1] = data_as_list[-1][:-1] - - for line in data_as_list: - yield line - - def mock_open(mock=None, read_data=''): - """ - A helper function to create a mock to replace the use of `open`. It works - for `open` called directly or used as a context manager. - - The `mock` argument is the mock object to configure. If `None` (the - default) then a `MagicMock` will be created for you, with the API limited - to methods or attributes available on standard file handles. - - `read_data` is a string for the `read` methoddline`, and `readlines` of the - file handle to return. This is an empty string by default. - """ - def _readlines_side_effect(*args, **kwargs): - if handle.readlines.return_value is not None: - return handle.readlines.return_value - return list(_data) - - def _read_side_effect(*args, **kwargs): - if handle.read.return_value is not None: - return handle.read.return_value - return type(read_data)().join(_data) - - def _readline_side_effect(): - if handle.readline.return_value is not None: - while True: - yield handle.readline.return_value - for line in _data: - yield line - - global file_spec - if file_spec is None: - import _io - file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) - - if mock is None: - mock = MagicMock(name='open', spec=open) - - handle = MagicMock(spec=file_spec) - handle.__enter__.return_value = handle - - _data = _iterate_read_data(read_data) - - handle.write.return_value = None - handle.read.return_value = None - handle.readline.return_value = None - handle.readlines.return_value = None - - handle.read.side_effect = _read_side_effect - handle.readline.side_effect = _readline_side_effect() - handle.readlines.side_effect = _readlines_side_effect - - mock.return_value = handle - return mock diff --git a/tests/unit/compat/unittest.py b/tests/unit/compat/unittest.py deleted file mode 100644 index 98f08ad..0000000 --- a/tests/unit/compat/unittest.py +++ /dev/null @@ -1,38 +0,0 @@ -# (c) 2014, Toshio Kuratomi -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -''' -Compat module for Python2.7's unittest module -''' - -import sys - -# Allow wildcard import because we really do want to import all of -# unittests's symbols into this compat shim -# pylint: disable=wildcard-import,unused-wildcard-import -if sys.version_info < (2, 7): - try: - # Need unittest2 on python2.6 - from unittest2 import * - except ImportError: - print('You need unittest2 installed on python2.6.x to run tests') -else: - from unittest import * diff --git a/tests/unit/plugins/module_utils/test__api_data.py b/tests/unit/plugins/module_utils/test__api_data.py new file mode 100644 index 0000000..4c0267e --- /dev/null +++ b/tests/unit/plugins/module_utils/test__api_data.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible_collections.community.routeros.plugins.module_utils._api_data import ( + VersionedAPIData, + KeyInfo, + split_path, + join_path, +) + + +def test_api_data_errors(): + with pytest.raises(ValueError) as exc: + VersionedAPIData() + assert exc.value.args[0] == 'fields must be provided' + + values = [ + ('primary_keys', []), + ('stratify_keys', []), + ('has_identifier', True), + ('single_value', True), + ('unknown_mechanism', True), + ] + + for index, (param, param_value) in enumerate(values): + for param2, param2_value in values[index + 1:]: + with pytest.raises(ValueError) as exc: + VersionedAPIData(**{param: param_value, param2: param2_value}) + assert exc.value.args[0] == 'primary_keys, stratify_keys, has_identifier, single_value, and unknown_mechanism are mutually exclusive' + + with pytest.raises(ValueError) as exc: + VersionedAPIData(unknown_mechanism=True, fully_understood=True) + assert exc.value.args[0] == 'unknown_mechanism and fully_understood cannot be combined' + + with pytest.raises(ValueError) as exc: + VersionedAPIData(unknown_mechanism=True, fixed_entries=True) + assert exc.value.args[0] == 'fixed_entries can only be used with primary_keys' + + with pytest.raises(ValueError) as exc: + VersionedAPIData(primary_keys=['foo'], fields={}) + assert exc.value.args[0] == 'Primary key foo must be in fields!' + + with pytest.raises(ValueError) as exc: + VersionedAPIData(stratify_keys=['foo'], fields={}) + assert exc.value.args[0] == 'Stratify key foo must be in fields!' + + with pytest.raises(ValueError) as exc: + VersionedAPIData(required_one_of=['foo'], fields={}) + assert exc.value.args[0] == 'Require one of element at index #1 must be a list!' + + with pytest.raises(ValueError) as exc: + VersionedAPIData(required_one_of=[['foo']], fields={}) + assert exc.value.args[0] == 'Require one of key foo must be in fields!' + + with pytest.raises(ValueError) as exc: + VersionedAPIData(mutually_exclusive=['foo'], fields={}) + assert exc.value.args[0] == 'Mutually exclusive element at index #1 must be a list!' + + with pytest.raises(ValueError) as exc: + VersionedAPIData(mutually_exclusive=[['foo']], fields={}) + assert exc.value.args[0] == 'Mutually exclusive key foo must be in fields!' + + +def test_key_info_errors(): + values = [ + ('required', True), + ('default', ''), + ('automatically_computed_from', ()), + ('can_disable', True), + ] + + params_allowed_together = [ + 'default', + 'can_disable', + ] + + emsg = 'required, default, automatically_computed_from, and can_disable are mutually exclusive besides default and can_disable which can be set together' + for index, (param, param_value) in enumerate(values): + for param2, param2_value in values[index + 1:]: + if param in params_allowed_together and param2 in params_allowed_together: + continue + with pytest.raises(ValueError) as exc: + KeyInfo(**{param: param_value, param2: param2_value}) + assert exc.value.args[0] == emsg + + with pytest.raises(ValueError) as exc: + KeyInfo('foo') + assert exc.value.args[0] == 'KeyInfo() does not have positional arguments' + + with pytest.raises(ValueError) as exc: + KeyInfo(remove_value='') + assert exc.value.args[0] == 'remove_value can only be specified if can_disable=True' + + with pytest.raises(ValueError) as exc: + KeyInfo(read_only=True, write_only=True) + assert exc.value.args[0] == 'read_only and write_only cannot be used at the same time' + + with pytest.raises(ValueError) as exc: + KeyInfo(read_only=True, default=0) + assert exc.value.args[0] == 'read_only can not be combined with can_disable, remove_value, absent_value, default, or required' + + +SPLIT_PATHS = [ + ('', [], ''), + (' ip ', ['ip'], 'ip'), + ('ip', ['ip'], 'ip'), + (' ip \t\n\raddress ', ['ip', 'address'], 'ip address'), +] + + +@pytest.mark.parametrize("joined_input, split, joined_output", SPLIT_PATHS) +def test_join_split_path(joined_input, split, joined_output): + assert split_path(joined_input) == split + assert join_path(split) == joined_output diff --git a/tests/unit/plugins/module_utils/test__api_helper.py b/tests/unit/plugins/module_utils/test__api_helper.py new file mode 100644 index 0000000..b909f38 --- /dev/null +++ b/tests/unit/plugins/module_utils/test__api_helper.py @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re +import sys + +import pytest + +from ansible_collections.community.routeros.plugins.module_utils._api_data import ( + PATHS, +) + +from ansible_collections.community.routeros.plugins.module_utils._api_helper import ( + _value_to_str, + _test_rule_except_invert, + validate_and_prepare_restrict, + restrict_entry_accepted, +) + + +VALUE_TO_STR = [ + (None, None), + ('', u''), + ('foo', u'foo'), + (True, u'true'), + (False, u'false'), + ([], u'[]'), + ({}, u'{}'), + (1, u'1'), + (-42, u'-42'), + (1.5, u'1.5'), + (1.0, u'1.0'), +] + + +@pytest.mark.parametrize("value, expected", VALUE_TO_STR) +def test_value_to_str(value, expected): + result = _value_to_str(value) + print(repr(result)) + assert result == expected + + +TEST_RULE_EXCEPT_INVERT = [ + ( + None, + { + 'field': u'foo', + 'match_disabled': False, + 'invert': False, + }, + False, + ), + ( + None, + { + 'field': u'foo', + 'match_disabled': True, + 'invert': False, + }, + True, + ), + ( + 1, + { + 'field': u'foo', + 'match_disabled': False, + 'invert': False, + 'values': [1], + }, + True, + ), + ( + 1, + { + 'field': u'foo', + 'match_disabled': False, + 'invert': False, + 'values': ['1'], + }, + False, + ), + ( + 1, + { + 'field': u'foo', + 'match_disabled': False, + 'invert': False, + 'regex': re.compile(u'^1$'), + 'regex_source': u'^1$', + }, + True, + ), + ( + 1.10, + { + 'field': u'foo', + 'match_disabled': False, + 'invert': False, + 'regex': re.compile(u'^1\\.1$'), + 'regex_source': u'^1\\.1$', + }, + True, + ), + ( + 10, + { + 'field': u'foo', + 'match_disabled': False, + 'invert': False, + 'regex': re.compile(u'^1$'), + 'regex_source': u'^1$', + }, + False, + ), +] + + +@pytest.mark.parametrize("value, rule, expected", TEST_RULE_EXCEPT_INVERT) +def test_rule_except_invert(value, rule, expected): + result = _test_rule_except_invert(value, rule) + print(repr(result)) + assert result == expected + + +_test_path = PATHS[('ip', 'firewall', 'filter')] +_test_path.provide_version('7.0') +TEST_PATH = _test_path.get_data() + + +class FailJsonExc(Exception): + def __init__(self, msg, kwargs): + self.msg = msg + self.kwargs = kwargs + + +class FakeModule(object): + def __init__(self, restrict_value): + self.params = { + 'restrict': restrict_value, + } + + def fail_json(self, msg, **kwargs): + raise FailJsonExc(msg, kwargs) + + +TEST_VALIDATE_AND_PREPARE_RESTRICT = [ + ( + [{ + 'field': u'chain', + 'match_disabled': False, + 'values': None, + 'regex': None, + 'invert': False, + }], + [{ + 'field': u'chain', + 'match_disabled': False, + 'invert': False, + }], + ), + ( + [{ + 'field': u'comment', + 'match_disabled': True, + 'values': None, + 'regex': None, + 'invert': False, + }], + [{ + 'field': u'comment', + 'match_disabled': True, + 'invert': False, + }], + ), + ( + [{ + 'field': u'comment', + 'match_disabled': False, + 'values': None, + 'regex': None, + 'invert': True, + }], + [{ + 'field': u'comment', + 'match_disabled': False, + 'invert': True, + }], + ), +] + +if sys.version_info >= (2, 7, 17): + # Somewhere between Python 2.7.15 (used by Ansible 3.9) and 2.7.17 (used by ansible-base 2.10) + # something changed with ``==`` for ``re.Pattern``, at least for some patterns + # (my guess is: for ``re.compile(u'')``) + TEST_VALIDATE_AND_PREPARE_RESTRICT.extend([ + ( + [ + { + 'field': u'comment', + 'match_disabled': False, + 'values': [], + 'regex': None, + 'invert': False, + }, + { + 'field': u'comment', + 'match_disabled': False, + 'values': [None, 1, 42.0, True, u'foo', [], {}], + 'regex': None, + 'invert': False, + }, + { + 'field': u'chain', + 'match_disabled': False, + 'values': None, + 'regex': u'', + 'invert': True, + }, + { + 'field': u'chain', + 'match_disabled': False, + 'values': None, + 'regex': u'foo', + 'invert': False, + }, + ], + [ + { + 'field': u'comment', + 'match_disabled': False, + 'invert': False, + 'values': [], + }, + { + 'field': u'comment', + 'match_disabled': False, + 'invert': False, + 'values': [None, 1, 42.0, True, u'foo', [], {}], + }, + { + 'field': u'chain', + 'match_disabled': False, + 'invert': True, + 'regex': re.compile(u''), + 'regex_source': u'', + }, + { + 'field': u'chain', + 'match_disabled': False, + 'invert': False, + 'regex': re.compile(u'foo'), + 'regex_source': u'foo', + }, + ], + ), + ]) + + +@pytest.mark.parametrize("restrict_value, expected", TEST_VALIDATE_AND_PREPARE_RESTRICT) +def test_validate_and_prepare_restrict(restrict_value, expected): + fake_module = FakeModule(restrict_value) + result = validate_and_prepare_restrict(fake_module, TEST_PATH) + print(repr(result)) + assert result == expected + + +TEST_VALIDATE_AND_PREPARE_RESTRICT_FAIL = [ + ( + [{ + 'field': u'!foo', + 'match_disabled': False, + 'values': None, + 'regex': None, + 'invert': False, + }], + ['restrict: the field name "!foo" must not start with "!"'], + ), + ( + [{ + 'field': u'foo', + 'match_disabled': False, + 'values': None, + 'regex': None, + 'invert': False, + }], + ['restrict: the field "foo" does not exist for this path'], + ), + ( + [{ + 'field': u'chain', + 'match_disabled': False, + 'values': None, + 'regex': u'(', + 'invert': False, + }], + [ + 'restrict: invalid regular expression "(": missing ), unterminated subpattern at position 0', + 'restrict: invalid regular expression "(": unbalanced parenthesis', + ] + ), +] + + +@pytest.mark.parametrize("restrict_value, expected", TEST_VALIDATE_AND_PREPARE_RESTRICT_FAIL) +def test_validate_and_prepare_restrict_fail(restrict_value, expected): + fake_module = FakeModule(restrict_value) + with pytest.raises(FailJsonExc) as exc: + validate_and_prepare_restrict(fake_module, TEST_PATH) + print(repr(exc.value.msg)) + assert exc.value.msg in expected + + +TEST_RESTRICT_ENTRY_ACCEPTED = [ + ( + { + 'chain': 'input', + }, + [ + { + 'field': u'chain', + 'match_disabled': False, + 'invert': False, + }, + ], + False, + ), + ( + { + 'chain': 'input', + }, + [ + { + 'field': u'chain', + 'match_disabled': False, + 'invert': True, + }, + ], + True, + ), + ( + { + 'comment': 'foo', + }, + [ + { + 'field': u'comment', + 'match_disabled': True, + 'invert': False, + }, + ], + False, + ), + ( + {}, + [ + { + 'field': u'comment', + 'match_disabled': True, + 'invert': False, + }, + ], + True, + ), +] + + +@pytest.mark.parametrize("entry, restrict_data, expected", TEST_RESTRICT_ENTRY_ACCEPTED) +def test_restrict_entry_accepted(entry, restrict_data, expected): + result = restrict_entry_accepted(entry, TEST_PATH, restrict_data) + print(repr(result)) + assert result == expected diff --git a/tests/unit/plugins/module_utils/test_quoting.py b/tests/unit/plugins/module_utils/test_quoting.py new file mode 100644 index 0000000..6d29d50 --- /dev/null +++ b/tests/unit/plugins/module_utils/test_quoting.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.routeros.plugins.module_utils.quoting import ( + ParseError, + convert_list_to_dictionary, + join_routeros_command, + parse_argument_value, + quote_routeros_argument, + quote_routeros_argument_value, + split_routeros_command, +) + + +TEST_PARSE_ARGUMENT_VALUE = [ + ('a', {}, ('a', 1)), + ('a ', {'must_match_everything': False}, ('a', 1)), + (r'"a b"', {}, ('a b', 5)), + (r'"b\"f"', {}, ('b"f', 6)), + (r'"\01"', {}, ('\x01', 5)), + (r'"\1F"', {}, ('\x1f', 5)), + (r'"\FF"', {}, (to_native(b'\xff'), 5)), + (r'"\"e"', {}, ('"e', 5)), + (r'"\""', {}, ('"', 4)), + (r'"\\"', {}, ('\\', 4)), + (r'"\?"', {}, ('?', 4)), + (r'"\$"', {}, ('$', 4)), + (r'"\_"', {}, (' ', 4)), + (r'"\a"', {}, ('\a', 4)), + (r'"\b"', {}, ('\b', 4)), + (r'"\f"', {}, (to_native(b'\xff'), 4)), + (r'"\n"', {}, ('\n', 4)), + (r'"\r"', {}, ('\r', 4)), + (r'"\t"', {}, ('\t', 4)), + (r'"\v"', {}, ('\v', 4)), + (r'"b=c"', {}, ('b=c', 5)), + (r'""', {}, ('', 2)), + (r'"" ', {'must_match_everything': False}, ('', 2)), + ("'e", {'start_index': 1}, ('e', 2)), +] + + +@pytest.mark.parametrize("command, kwargs, result", TEST_PARSE_ARGUMENT_VALUE) +def test_parse_argument_value(command, kwargs, result): + result_ = parse_argument_value(command, **kwargs) + print(result_, result) + assert result_ == result + + +TEST_PARSE_ARGUMENT_VALUE_ERRORS = [ + (r'"e', {}, 'Unexpected end of string during escaped parameter'), + ("'e", {}, '"\'" can only be used inside double quotes'), + (r'\FF', {}, 'Escape sequences can only be used inside double quotes'), + (r'\"e', {}, 'Escape sequences can only be used inside double quotes'), + ('e=f', {}, '"=" can only be used inside double quotes'), + ('e$', {}, '"$" can only be used inside double quotes'), + ('e(', {}, '"(" can only be used inside double quotes'), + ('e)', {}, '")" can only be used inside double quotes'), + ('e[', {}, '"[" can only be used inside double quotes'), + ('e{', {}, '"{" can only be used inside double quotes'), + ('e`', {}, '"`" can only be used inside double quotes'), + ('?', {}, '"?" can only be used in escaped form'), + (r'b"', {}, '\'"\' must not appear in an unquoted value'), + (r'""a', {}, "Ending '\"' must be followed by space or end of string"), + (r'"" ', {}, "Unexpected data at end of value"), + ('"\\', {}, r"'\' must not be at the end of the line"), + (r'"\A', {}, r'Hex escape sequence cut off at end of line'), + (r'"\Z"', {}, r"Invalid escape sequence '\Z'"), + (r'"\Aa"', {}, r"Invalid hex escape sequence '\Aa'"), +] + + +@pytest.mark.parametrize("command, kwargs, message", TEST_PARSE_ARGUMENT_VALUE_ERRORS) +def test_parse_argument_value_errors(command, kwargs, message): + with pytest.raises(ParseError) as exc: + parse_argument_value(command, **kwargs) + print(exc.value.args[0], message) + assert exc.value.args[0] == message + + +TEST_SPLIT_ROUTEROS_COMMAND = [ + ('', []), + (' ', []), + (r'a b c', ['a', 'b', 'c']), + (r'a=b c d=e', ['a=b', 'c', 'd=e']), + (r'a="b f" c d=e', ['a=b f', 'c', 'd=e']), + (r'a="b\"f" c="\FF" d="\"e"', ['a=b"f', to_native(b'c=\xff'), 'd="e']), + (r'a="b=c"', ['a=b=c']), + (r'a=b ', ['a=b']), +] + + +@pytest.mark.parametrize("command, result", TEST_SPLIT_ROUTEROS_COMMAND) +def test_split_routeros_command(command, result): + result_ = split_routeros_command(command) + print(result_, result) + assert result_ == result + + +TEST_SPLIT_ROUTEROS_COMMAND_ERRORS = [ + (r'a=', 'Expected value, but found end of string'), + (r'a="b\"f" d="e', 'Unexpected end of string during escaped parameter'), + ('d=\'e', '"\'" can only be used inside double quotes'), + (r'c\FF', r'Found unexpected "\"'), + (r'd=\"e', 'Escape sequences can only be used inside double quotes'), + ('d=e=f', '"=" can only be used inside double quotes'), + ('d=e$', '"$" can only be used inside double quotes'), + ('d=e(', '"(" can only be used inside double quotes'), + ('d=e)', '")" can only be used inside double quotes'), + ('d=e[', '"[" can only be used inside double quotes'), + ('d=e{', '"{" can only be used inside double quotes'), + ('d=e`', '"`" can only be used inside double quotes'), + ('d=?', '"?" can only be used in escaped form'), + (r'a=b"', '\'"\' must not appear in an unquoted value'), + (r'a=""a', "Ending '\"' must be followed by space or end of string"), + ('a="\\', r"'\' must not be at the end of the line"), + (r'a="\Z', r"Invalid escape sequence '\Z'"), + (r'a="\Aa', r"Invalid hex escape sequence '\Aa'"), +] + + +@pytest.mark.parametrize("command, message", TEST_SPLIT_ROUTEROS_COMMAND_ERRORS) +def test_split_routeros_command_errors(command, message): + with pytest.raises(ParseError) as exc: + split_routeros_command(command) + print(exc.value.args[0], message) + assert exc.value.args[0] == message + + +TEST_CONVERT_LIST_TO_DICTIONARY = [ + (['a=b', 'c=d=e', 'e='], {}, {'a': 'b', 'c': 'd=e', 'e': ''}), + (['a=b', 'c=d=e', 'e='], {'skip_empty_values': False}, {'a': 'b', 'c': 'd=e', 'e': ''}), + (['a=b', 'c=d=e', 'e='], {'skip_empty_values': True}, {'a': 'b', 'c': 'd=e'}), + (['a=b', 'c=d=e', 'e=', 'f'], {'require_assignment': False}, {'a': 'b', 'c': 'd=e', 'e': '', 'f': None}), +] + + +@pytest.mark.parametrize("list, kwargs, expected_dict", TEST_CONVERT_LIST_TO_DICTIONARY) +def test_convert_list_to_dictionary(list, kwargs, expected_dict): + result = convert_list_to_dictionary(list, **kwargs) + print(result, expected_dict) + assert result == expected_dict + + +TEST_CONVERT_LIST_TO_DICTIONARY_ERRORS = [ + (['a=b', 'c=d=e', 'e=', 'f'], {}, "missing '=' after 'f'"), +] + + +@pytest.mark.parametrize("list, kwargs, message", TEST_CONVERT_LIST_TO_DICTIONARY_ERRORS) +def test_convert_list_to_dictionary_errors(list, kwargs, message): + with pytest.raises(ParseError) as exc: + result = convert_list_to_dictionary(list, **kwargs) + print(exc.value.args[0], message) + assert exc.value.args[0] == message + + +TEST_JOIN_ROUTEROS_COMMAND = [ + (['a=b', 'c=d=e', 'e=', 'f', 'g=h i j', 'h="h"'], r'a=b c="d=e" e="" f g="h\_i\_j" h="\"h\""'), +] + + +@pytest.mark.parametrize("list, expected", TEST_JOIN_ROUTEROS_COMMAND) +def test_join_routeros_command(list, expected): + result = join_routeros_command(list) + print(result, expected) + assert result == expected + + +TEST_QUOTE_ROUTEROS_ARGUMENT = [ + (r'', r''), + (r'a', r'a'), + (r'a=b', r'a=b'), + (r'a=b c', r'a="b\_c"'), + (r'a="b c"', r'a="\"b\_c\""'), + (r"a='b", "a=\"'b\""), + (r"a=b'", "a=\"b'\""), + (r'a=""', r'a="\"\""'), +] + + +@pytest.mark.parametrize("argument, expected", TEST_QUOTE_ROUTEROS_ARGUMENT) +def test_quote_routeros_argument(argument, expected): + result = quote_routeros_argument(argument) + print(result, expected) + assert result == expected + + +TEST_QUOTE_ROUTEROS_ARGUMENT_ERRORS = [ + ('a b', 'Attribute names must not contain spaces'), + ('a b=c', 'Attribute names must not contain spaces'), +] + + +@pytest.mark.parametrize("argument, message", TEST_QUOTE_ROUTEROS_ARGUMENT_ERRORS) +def test_quote_routeros_argument_errors(argument, message): + with pytest.raises(ParseError) as exc: + result = quote_routeros_argument(argument) + print(exc.value.args[0], message) + assert exc.value.args[0] == message + + +TEST_QUOTE_ROUTEROS_ARGUMENT_VALUE = [ + (r'', r'""'), + (r";", r'";"'), + (r" ", r'"\_"'), + (r"=", r'"="'), + (r'a', r'a'), + (r'a=b', r'"a=b"'), + (r'b c', r'"b\_c"'), + (r'"b c"', r'"\"b\_c\""'), + ("'b", "\"'b\""), + ("b'", "\"b'\""), + ('"', r'"\""'), + ('\\', r'"\\"'), + ('?', r'"\?"'), + ('$', r'"\$"'), + ('_', r'_'), + (' ', r'"\_"'), + ('\a', r'"\a"'), + ('\b', r'"\b"'), + # (to_native(b'\xff'), r'"\f"'), + ('\n', r'"\n"'), + ('\r', r'"\r"'), + ('\t', r'"\t"'), + ('\v', r'"\v"'), + ('\x01', r'"\01"'), + ('\x1f', r'"\1F"'), +] + + +@pytest.mark.parametrize("argument, expected", TEST_QUOTE_ROUTEROS_ARGUMENT_VALUE) +def test_quote_routeros_argument_value(argument, expected): + result = quote_routeros_argument_value(argument) + print(result, expected) + assert result == expected + + +TEST_ROUNDTRIP = [ + {'a': 'b', 'c': 'd'}, + {'script': ''':local host value=[/system identity get name]; +:local date value=[/system clock get date]; +:local day [ :pick $date 4 6 ]; +:local month [ :pick $date 0 3 ]; +:local year [ :pick $date 7 11 ]; +:local name value=($host."-".$day."-".$month."-".$year); +/system backup save name=$name; +/export file=$name; +/tool fetch address="192.168.1.1" user=ros password="PASSWORD" mode=ftp dst-path=("/mikrotik/rsc/".$name.".rsc") src-path=($name.".rsc") upload=yes; +/tool fetch address="192.168.1.1" user=ros password="PASSWORD" mode=ftp dst-path=("/mikrotik/backup/".$name.".backup") src-path=($name.".backup") upload=yes; +'''}, +] + + +@pytest.mark.parametrize("dictionary", TEST_ROUNDTRIP) +def test_roundtrip(dictionary): + argument_list = ['%s=%s' % (k, v) for k, v in dictionary.items()] + command = join_routeros_command(argument_list) + resplit_list = split_routeros_command(command) + print(resplit_list, argument_list) + assert resplit_list == argument_list + re_dictionary = convert_list_to_dictionary(resplit_list) + print(re_dictionary, dictionary) + assert re_dictionary == dictionary diff --git a/tests/unit/plugins/modules/__init__.py b/tests/unit/plugins/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/plugins/modules/fake_api.py b/tests/unit/plugins/modules/fake_api.py new file mode 100644 index 0000000..cbcd237 --- /dev/null +++ b/tests/unit/plugins/modules/fake_api.py @@ -0,0 +1,276 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.routeros.plugins.module_utils._api_data import PATHS + + +FAKE_ROS_VERSION = '7.5.0' + + +class FakeLibRouterosError(Exception): + def __init__(self, message): + self.message = message + super(FakeLibRouterosError, self).__init__(self.message) + + +class TrapError(FakeLibRouterosError): + def __init__(self, message='failure: already have interface with such name'): + super(TrapError, self).__init__(message) + + +# fixtures +class fake_ros_api(object): + def __init__(self, api, path): + pass + + @classmethod + def path(cls, api, path): + fake_bridge = [{".id": "*DC", "name": "b2", "mtu": "auto", "actual-mtu": 1500, + "l2mtu": 65535, "arp": "enabled", "arp-timeout": "auto", + "mac-address": "3A:C1:90:D6:E8:44", "protocol-mode": "rstp", + "fast-forward": "true", "igmp-snooping": "false", + "auto-mac": "true", "ageing-time": "5m", "priority": + "0x8000", "max-message-age": "20s", "forward-delay": "15s", + "transmit-hold-count": 6, "vlan-filtering": "false", + "dhcp-snooping": "false", "running": "true", "disabled": "false"}] + return fake_bridge + + @classmethod + def arbitrary(cls, api, path): + def retr(self, *args, **kwargs): + if 'name' not in kwargs.keys(): + raise TrapError(message="no such command") + dummy_test_string = '/interface/bridge add name=unit_test_brige_arbitrary' + result = "/%s/%s add name=%s" % (path[0], path[1], kwargs['name']) + return [result] + return retr + + def add(self, name): + if name == "unit_test_brige_exist": + raise TrapError + return '*A1' + + def remove(self, id): + if id != "*A1": + raise TrapError(message="no such item (4)") + return '*A1' + + def update(self, **kwargs): + if kwargs['.id'] != "*A1" or 'name' not in kwargs.keys(): + raise TrapError(message="no such item (4)") + return ["updated: {'.id': '%s' % kwargs['.id'], 'name': '%s' % kwargs['name']}"] + + def select(self, *args): + dummy_bridge = [{".id": "*A1", "name": "dummy_bridge_A1"}, + {".id": "*A2", "name": "dummy_bridge_A2"}, + {".id": "*A3", "name": "dummy_bridge_A3"}] + + result = [] + for dummy in dummy_bridge: + found = {} + for search in args: + if search in dummy.keys(): + found[search] = dummy[search] + else: + continue + if len(found.keys()) == 2: + result.append(found) + + if result: + return result + else: + return [] + + @classmethod + def select_where(cls, api, path): + api_path = Where() + return api_path + + +class Where(object): + def __init__(self): + pass + + def select(self, *args): + return self + + def where(self, *args): + return [{".id": "*A1", "name": "dummy_bridge_A1"}] + + +class Key(object): + def __init__(self, name): + self.name = name + self.str_return() + + def str_return(self): + return str(self.name) + + +class Or(object): + def __init__(self, *args): + self.args = args + self.str_return() + + def str_return(self): + return repr(self.args) + + +def _normalize_entry(entry, path_info, on_create=False): + for key, data in path_info.fields.items(): + if key not in entry and data.default is not None and (not data.can_disable or on_create): + entry[key] = data.default + if data.can_disable: + if key in entry and entry[key] in (None, data.remove_value): + del entry[key] + if ('!%s' % key) in entry: + entry.pop(key, None) + del entry['!%s' % key] + if data.absent_value is not None and key in entry and entry[key] == data.absent_value: + del entry[key] + + +def massage_expected_result_data(values, path, keep_all=False, remove_dynamic=False, remove_builtin=False): + versioned_path_info = PATHS[path] + versioned_path_info.provide_version(FAKE_ROS_VERSION) + path_info = versioned_path_info.get_data() + if remove_dynamic: + values = [entry for entry in values if not entry.get('dynamic', False)] + if remove_builtin: + values = [entry for entry in values if not entry.get('builtin', False)] + values = [entry.copy() for entry in values] + for entry in values: + _normalize_entry(entry, path_info) + if not keep_all: + for key in list(entry): + if key == '.id' or key in path_info.fields: + continue + del entry[key] + for key, data in path_info.fields.items(): + if data.absent_value is not None and key not in entry: + entry[key] = data.absent_value + return values + + +class Path(object): + def __init__(self, path, initial_values, read_only=False): + self._path = path + versioned_path_info = PATHS[path] + versioned_path_info.provide_version(FAKE_ROS_VERSION) + self._path_info = versioned_path_info.get_data() + self._values = [entry.copy() for entry in initial_values] + for entry in self._values: + _normalize_entry(entry, self._path_info) + self._new_id_counter = 0 + self._read_only = read_only + + def _sanitize(self, entry): + entry = entry.copy() + for field, field_info in self._path_info.fields.items(): + if field in entry: + if field_info.write_only: + del entry[field] + return entry + + def __iter__(self): + return [self._sanitize(entry) for entry in self._values].__iter__() + + def _find_id(self, id, required=False): + for index, entry in enumerate(self._values): + if entry['.id'] == id: + return index + if required: + raise FakeLibRouterosError('Cannot find key "%s"' % id) + return None + + def add(self, **kwargs): + if self._path_info.fixed_entries or self._path_info.single_value: + raise Exception('Cannot add entries') + if self._read_only: + raise Exception('Modifying read-only path: add %s' % repr(kwargs)) + if '.id' in kwargs: + raise Exception('Trying to create new entry with ".id" field: %s' % repr(kwargs)) + if 'dynamic' in kwargs or 'builtin' in kwargs: + raise Exception('Trying to add a dynamic or builtin entry') + self._new_id_counter += 1 + id = '*NEW%d' % self._new_id_counter + entry = { + '.id': id, + } + for field, value in kwargs.items(): + if field.startswith('!'): + field = field[1:] + if field not in self._path_info.fields: + raise ValueError('Trying to set unknown field "{field}"'.format(field=field)) + field_info = self._path_info.fields[field] + if field_info.read_only: + raise ValueError('Trying to set read-only field "{field}"'.format(field=field)) + entry[field] = value + _normalize_entry(entry, self._path_info, on_create=True) + self._values.append(entry) + return id + + def remove(self, *args): + if self._path_info.fixed_entries or self._path_info.single_value: + raise Exception('Cannot remove entries') + if self._read_only: + raise Exception('Modifying read-only path: remove %s' % repr(args)) + for id in args: + index = self._find_id(id, required=True) + entry = self._values[index] + if entry.get('dynamic', False) or entry.get('builtin', False): + raise Exception('Trying to remove a dynamic or builtin entry') + del self._values[index] + + def update(self, **kwargs): + if self._read_only: + raise Exception('Modifying read-only path: update %s' % repr(kwargs)) + if 'dynamic' in kwargs or 'builtin' in kwargs: + raise Exception('Trying to update dynamic builtin fields') + if self._path_info.single_value: + index = 0 + else: + index = self._find_id(kwargs['.id'], required=True) + entry = self._values[index] + if entry.get('dynamic', False) or entry.get('builtin', False): + raise Exception('Trying to update a dynamic or builtin entry') + for field in kwargs: + if field == '.id': + continue + if field.startswith('!'): + field = field[1:] + if field not in self._path_info.fields: + raise ValueError('Trying to update unknown field "{field}"'.format(field=field)) + field_info = self._path_info.fields[field] + if field_info.read_only: + raise ValueError('Trying to update read-only field "{field}"'.format(field=field)) + entry.update(kwargs) + _normalize_entry(entry, self._path_info) + + def __call__(self, command, *args, **kwargs): + if self._read_only: + raise Exception('Modifying read-only path: "%s" %s %s' % (command, repr(args), repr(kwargs))) + if command != 'move': + raise FakeLibRouterosError('Unsupported command "%s"' % command) + if self._path_info.fixed_entries or self._path_info.single_value: + raise Exception('Cannot move entries') + yield None # make sure that nothing happens if the result isn't consumed + source_index = self._find_id(kwargs.pop('numbers'), required=True) + entry = self._values.pop(source_index) + dest_index = self._find_id(kwargs.pop('destination'), required=True) + self._values.insert(dest_index, entry) + + +def create_fake_path(path, initial_values, read_only=False): + def create(api, called_path): + called_path = tuple(called_path) + if path != called_path: + raise AssertionError('Expected {path}, got {called_path}'.format(path=path, called_path=called_path)) + return Path(path, initial_values, read_only=read_only) + + return create diff --git a/tests/unit/plugins/modules/fixtures/__init__.py b/tests/unit/plugins/modules/fixtures/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/plugins/modules/fixtures/facts/export.license b/tests/unit/plugins/modules/fixtures/facts/export.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/export.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/export_verbose.license b/tests/unit/plugins/modules/fixtures/facts/export_verbose.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/export_verbose.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license b/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license b/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/system_package_print.license b/tests/unit/plugins/modules/fixtures/system_package_print.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/system_package_print.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/fixtures/system_resource_print.license b/tests/unit/plugins/modules/fixtures/system_resource_print.license new file mode 100644 index 0000000..edff8c7 --- /dev/null +++ b/tests/unit/plugins/modules/fixtures/system_resource_print.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/unit/plugins/modules/routeros_module.py b/tests/unit/plugins/modules/routeros_module.py index 9cad18e..4786da6 100644 --- a/tests/unit/plugins/modules/routeros_module.py +++ b/tests/unit/plugins/modules/routeros_module.py @@ -1,19 +1,6 @@ -# (c) 2016 Red Hat Inc. -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . +# Copyright (c) 2016 Red Hat Inc. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) @@ -22,7 +9,7 @@ __metaclass__ = type import os import json -from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') diff --git a/tests/unit/plugins/modules/test_api.py b/tests/unit/plugins/modules/test_api.py index cabb918..9a03825 100644 --- a/tests/unit/plugins/modules/test_api.py +++ b/tests/unit/plugins/modules/test_api.py @@ -1,145 +1,42 @@ -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import json -import pytest +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch, MagicMock +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase -from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock -from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, Or, fake_ros_api from ansible_collections.community.routeros.plugins.modules import api -class FakeLibRouterosError(Exception): - def __init__(self, message): - self.message = message - super(FakeLibRouterosError, self).__init__(self.message) - - -class TrapError(FakeLibRouterosError): - def __init__(self, message="failure: already have interface with such name"): - super(TrapError, self).__init__(message) - - -# fixtures -class fake_ros_api(object): - def __init__(self, api, path): - pass - - def path(self, api, path): - fake_bridge = [{".id": "*DC", "name": "b2", "mtu": "auto", "actual-mtu": 1500, - "l2mtu": 65535, "arp": "enabled", "arp-timeout": "auto", - "mac-address": "3A:C1:90:D6:E8:44", "protocol-mode": "rstp", - "fast-forward": "true", "igmp-snooping": "false", - "auto-mac": "true", "ageing-time": "5m", "priority": - "0x8000", "max-message-age": "20s", "forward-delay": "15s", - "transmit-hold-count": 6, "vlan-filtering": "false", - "dhcp-snooping": "false", "running": "true", "disabled": "false"}] - return fake_bridge - - def arbitrary(self, api, path): - def retr(self, *args, **kwargs): - if 'name' not in kwargs.keys(): - raise TrapError(message="no such command") - dummy_test_string = '/interface/bridge add name=unit_test_brige_arbitrary' - result = "/%s/%s add name=%s" % (path[0], path[1], kwargs['name']) - return [result] - return retr - - def add(self, name): - if name == "unit_test_brige_exist": - raise TrapError - return '*A1' - - def remove(self, id): - if id != "*A1": - raise TrapError(message="no such item (4)") - return '*A1' - - def update(self, **kwargs): - if kwargs['.id'] != "*A1" or 'name' not in kwargs.keys(): - raise TrapError(message="no such item (4)") - return ["updated: {'.id': '%s' % kwargs['.id'], 'name': '%s' % kwargs['name']}"] - - def select(self, *args): - dummy_bridge = [{".id": "*A1", "name": "dummy_bridge_A1"}, - {".id": "*A2", "name": "dummy_bridge_A2"}, - {".id": "*A3", "name": "dummy_bridge_A3"}] - - result = [] - for dummy in dummy_bridge: - found = {} - for search in args: - if search in dummy.keys(): - found[search] = dummy[search] - else: - continue - if len(found.keys()) == 2: - result.append(found) - - if result: - return result - else: - return ["no results for 'interface bridge 'query' %s" % ' '.join(args)] - - def select_where(self, api, path): - api_path = Where() - return api_path - - -class Where(object): - def __init__(self): - pass - - def select(self, *args): - return self - - def where(self, *args): - return ["*A1"] - - -class Key(object): - def __init__(self, name): - self.name = name - self.str_return() - - def str_return(self): - return str(self.name) - - class TestRouterosApiModule(ModuleTestCase): def setUp(self): super(TestRouterosApiModule, self).setUp() - librouteros = pytest.importorskip("librouteros") self.module = api self.module.LibRouterosError = FakeLibRouterosError self.module.connect = MagicMock(new=fake_ros_api) + self.module.check_has_library = MagicMock() + self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api.create_api', MagicMock(new=fake_ros_api)) + self.patch_create_api.start() self.module.Key = MagicMock(new=Key) + self.module.Or = MagicMock(new=Or) self.config_module_args = {"username": "admin", "password": "pаss", "hostname": "127.0.0.1", "path": "interface bridge"} + def tearDown(self): + self.patch_create_api.stop() + def test_module_fail_when_required_args_missing(self): with self.assertRaises(AnsibleFailJson) as exc: - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['failed'], True) @@ -147,8 +44,8 @@ class TestRouterosApiModule(ModuleTestCase): @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.path) def test_api_path(self): with self.assertRaises(AnsibleExitJson) as exc: - set_module_args(self.config_module_args.copy()) - self.module.main() + with set_module_args(self.config_module_args.copy()): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], False) @@ -158,8 +55,8 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleExitJson) as exc: module_args = self.config_module_args.copy() module_args['add'] = "name=unit_test_brige" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], True) @@ -169,8 +66,8 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleFailJson) as exc: module_args = self.config_module_args.copy() module_args['add'] = "name=unit_test_brige_exist" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['failed'], True) @@ -181,8 +78,8 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleExitJson) as exc: module_args = self.config_module_args.copy() module_args['remove'] = "*A1" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], True) @@ -192,8 +89,8 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleFailJson) as exc: module_args = self.config_module_args.copy() module_args['remove'] = "*A2" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['failed'], True) @@ -204,8 +101,8 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleExitJson) as exc: module_args = self.config_module_args.copy() module_args['cmd'] = "add name=unit_test_brige_arbitrary" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], False) @@ -215,8 +112,8 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleFailJson) as exc: module_args = self.config_module_args.copy() module_args['cmd'] = "add NONE_EXIST=unit_test_brige_arbitrary" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['failed'], True) @@ -227,8 +124,8 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleExitJson) as exc: module_args = self.config_module_args.copy() module_args['update'] = ".id=*A1 name=unit_test_brige" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], True) @@ -238,8 +135,8 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleFailJson) as exc: module_args = self.config_module_args.copy() module_args['update'] = ".id=*A2 name=unit_test_brige" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['failed'], True) @@ -250,41 +147,163 @@ class TestRouterosApiModule(ModuleTestCase): with self.assertRaises(AnsibleExitJson) as exc: module_args = self.config_module_args.copy() module_args['query'] = ".id name" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + {'.id': '*A2', 'name': 'dummy_bridge_A2'}, + {'.id': '*A3', 'name': 'dummy_bridge_A3'}, + ]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) def test_api_query_missing_key(self): with self.assertRaises(AnsibleExitJson) as exc: module_args = self.config_module_args.copy() module_args['query'] = ".id other" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], ["no results for 'interface bridge 'query' .id other"]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) def test_api_query_and_WHERE(self): with self.assertRaises(AnsibleExitJson) as exc: module_args = self.config_module_args.copy() module_args['query'] = ".id name WHERE name == dummy_bridge_A2" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) def test_api_query_and_WHERE_no_cond(self): with self.assertRaises(AnsibleExitJson) as exc: module_args = self.config_module_args.copy() module_args['query'] = ".id name WHERE name != dummy_bridge_A2" - set_module_args(module_args) - self.module.main() + with set_module_args(module_args): + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_extended_query(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'name'], + } + with set_module_args(module_args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + {'.id': '*A2', 'name': 'dummy_bridge_A2'}, + {'.id': '*A3', 'name': 'dummy_bridge_A3'}, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_extended_query_missing_key(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'other'], + } + with set_module_args(module_args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], []) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) + def test_api_extended_query_and_WHERE(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'name'], + 'where': [ + { + 'attribute': 'name', + 'is': '==', + 'value': 'dummy_bridge_A2', + }, + ], + } + with set_module_args(module_args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) + def test_api_extended_query_and_WHERE_no_cond(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'name'], + 'where': [ + { + 'attribute': 'name', + 'is': 'not', + 'value': 'dummy_bridge_A2', + }, + ], + } + with set_module_args(module_args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.select_where) + def test_api_extended_query_and_WHERE_or(self): + with self.assertRaises(AnsibleExitJson) as exc: + module_args = self.config_module_args.copy() + module_args['extended_query'] = { + 'attributes': ['.id', 'name'], + 'where': [ + { + 'or': [ + { + 'attribute': 'name', + 'is': 'in', + 'value': [1, 2], + }, + { + 'attribute': 'name', + 'is': '!=', + 'value': 5, + }, + ], + }, + ], + } + with set_module_args(module_args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['msg'], [ + {'.id': '*A1', 'name': 'dummy_bridge_A1'}, + ]) diff --git a/tests/unit/plugins/modules/test_api_facts.py b/tests/unit/plugins/modules/test_api_facts.py new file mode 100644 index 0000000..7b019f9 --- /dev/null +++ b/tests/unit/plugins/modules/test_api_facts.py @@ -0,0 +1,753 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch, MagicMock +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase + +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api +from ansible_collections.community.routeros.plugins.modules import api_facts + + +API_RESPONSES = { + ('interface', ): [ + { + '.id': '*1', + 'name': 'first-ether', + 'default-name': 'ether1', + 'type': 'ether', + 'mtu': 1500, + 'actual-mtu': 1500, + 'l2mtu': 1598, + 'max-l2mtu': 4074, + 'mac-address': '00:11:22:33:44:55', + 'last-link-up-time': 'apr/22/2022 07:54:55', + 'link-downs': 0, + 'rx-byte': 1234, + 'tx-byte': 1234, + 'rx-packet': 1234, + 'tx-packet': 1234, + 'rx-drop': 1234, + 'tx-drop': 1234, + 'tx-queue-drop': 1234, + 'rx-error': 1234, + 'tx-error': 1234, + 'fp-rx-byte': 1234, + 'fp-tx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-packet': 1234, + 'running': True, + 'disabled': False, + }, + { + '.id': '*2', + 'name': 'second-ether', + 'default-name': 'ether2', + 'type': 'ether', + 'mtu': 1500, + 'actual-mtu': 1500, + 'l2mtu': 1598, + 'max-l2mtu': 4074, + 'mac-address': '00:11:22:33:44:66', + 'last-link-up-time': 'apr/22/2022 07:54:55', + 'link-downs': 0, + 'rx-byte': 1234, + 'tx-byte': 1234, + 'rx-packet': 1234, + 'tx-packet': 1234, + 'rx-drop': 1234, + 'tx-drop': 1234, + 'tx-queue-drop': 1234, + 'rx-error': 1234, + 'tx-error': 1234, + 'fp-rx-byte': 1234, + 'fp-tx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-packet': 1234, + 'running': True, + 'slave': True, + 'disabled': False, + }, + { + '.id': '*3', + 'name': 'third-ether', + 'default-name': 'ether3', + 'type': 'ether', + 'mtu': 1500, + 'actual-mtu': 1500, + 'l2mtu': 1598, + 'max-l2mtu': 4074, + 'mac-address': '00:11:22:33:44:77', + 'last-link-up-time': 'apr/22/2022 07:54:55', + 'link-downs': 0, + 'rx-byte': 1234, + 'tx-byte': 1234, + 'rx-packet': 1234, + 'tx-packet': 1234, + 'rx-drop': 1234, + 'tx-drop': 1234, + 'tx-queue-drop': 1234, + 'rx-error': 1234, + 'tx-error': 1234, + 'fp-rx-byte': 1234, + 'fp-tx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-packet': 1234, + 'running': True, + 'slave': True, + 'disabled': False, + }, + { + '.id': '*4', + 'name': 'fourth-ether', + 'default-name': 'ether4', + 'type': 'ether', + 'mtu': 1500, + 'actual-mtu': 1500, + 'l2mtu': 1598, + 'max-l2mtu': 4074, + 'mac-address': '00:11:22:33:44:88', + 'last-link-down-time': 'apr/23/2022 08:22:50', + 'last-link-up-time': 'apr/23/2022 08:22:52', + 'link-downs': 2, + 'rx-byte': 1234, + 'tx-byte': 1234, + 'rx-packet': 1234, + 'tx-packet': 1234, + 'rx-drop': 1234, + 'tx-drop': 1234, + 'tx-queue-drop': 1234, + 'rx-error': 1234, + 'tx-error': 1234, + 'fp-rx-byte': 1234, + 'fp-tx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-packet': 1234, + 'running': True, + 'disabled': False, + }, + { + '.id': '*5', + 'name': 'fifth-ether', + 'default-name': 'ether5', + 'type': 'ether', + 'mtu': 1500, + 'actual-mtu': 1500, + 'l2mtu': 1598, + 'max-l2mtu': 4074, + 'mac-address': '00:11:22:33:44:99', + 'last-link-down-time': 'may/02/2022 18:12:32', + 'last-link-up-time': 'may/02/2022 18:08:01', + 'link-downs': 14, + 'rx-byte': 1234, + 'tx-byte': 1234, + 'rx-packet': 1234, + 'tx-packet': 1234, + 'rx-drop': 1234, + 'tx-drop': 1234, + 'tx-queue-drop': 1234, + 'rx-error': 1234, + 'tx-error': 1234, + 'fp-rx-byte': 1234, + 'fp-tx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-packet': 1234, + 'running': False, + 'slave': True, + 'disabled': False, + }, + { + '.id': '*7', + 'name': 'my-bridge', + 'type': 'bridge', + 'mtu': 'auto', + 'actual-mtu': 1500, + 'l2mtu': 1598, + 'mac-address': '00:11:22:33:44:66', + 'last-link-up-time': 'apr/22/2022 07:54:48', + 'link-downs': 0, + 'rx-byte': 1234, + 'tx-byte': 1234, + 'rx-packet': 1234, + 'tx-packet': 1234, + 'rx-drop': 1234, + 'tx-drop': 1234, + 'tx-queue-drop': 1234, + 'rx-error': 1234, + 'tx-error': 1234, + 'fp-rx-byte': 1234, + 'fp-tx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-packet': 1234, + 'running': True, + 'disabled': False, + }, + ], + ('ip', 'address', ): [ + { + '.id': '*1', + 'address': '192.168.1.1/24', + 'network': '192.168.1.0', + 'interface': 'my-bridge', + 'actual-interface': 'my-bridge', + 'invalid': False, + 'dynamic': False, + 'disabled': False, + 'comment': 'Wohnung', + }, + { + '.id': '*5', + 'address': '192.168.2.1/24', + 'network': '192.168.2.0', + 'interface': 'fourth-ether', + 'actual-interface': 'fourth-ether', + 'invalid': False, + 'dynamic': False, + 'disabled': False, + 'comment': 'VoIP', + }, + { + '.id': '*6', + 'address': '1.2.3.4/21', + 'network': '84.73.216.0', + 'interface': 'first-ether', + 'actual-interface': 'first-ether', + 'invalid': False, + 'dynamic': True, + 'disabled': False, + }, + ], + ('ipv6', 'address', ): [ + { + '.id': '*1', + 'address': 'fe80::1:2:3/64', + 'from-pool': '', + 'interface': 'my-bridge', + 'actual-interface': 'my-bridge', + 'eui-64': False, + 'advertise': False, + 'no-dad': False, + 'invalid': False, + 'dynamic': True, + 'link-local': True, + 'disabled': False, + }, + { + '.id': '*2', + 'address': 'fe80::1:2:4/64', + 'from-pool': '', + 'interface': 'fourth-ether', + 'actual-interface': 'fourth-ether', + 'eui-64': False, + 'advertise': False, + 'no-dad': False, + 'invalid': False, + 'dynamic': True, + 'link-local': True, + 'disabled': False, + }, + { + '.id': '*3', + 'address': 'fe80::1:2:5/64', + 'from-pool': '', + 'interface': 'first-ether', + 'actual-interface': 'first-ether', + 'eui-64': False, + 'advertise': False, + 'no-dad': False, + 'invalid': False, + 'dynamic': True, + 'link-local': True, + 'disabled': False, + }, + ], + ('ip', 'neighbor', ): [], + ('system', 'identity', ): [ + { + 'name': 'MikroTik', + }, + ], + ('system', 'resource', ): [ + { + 'uptime': '2w3d4h5m6s', + 'version': '6.49.6 (stable)', + 'build-time': 'Apr/07/2022 17:53:31', + 'free-memory': 12345678, + 'total-memory': 23456789, + 'cpu': 'MIPS 24Kc V7.4', + 'cpu-count': 1, + 'cpu-frequency': 400, + 'cpu-load': 48, + 'free-hdd-space': 123456789, + 'total-hdd-space': 234567890, + 'write-sect-since-reboot': 1234, + 'write-sect-total': 12345, + 'bad-blocks': 0, + 'architecture-name': 'mipsbe', + 'board-name': 'RB750GL', + 'platform': 'MikroTik', + }, + ], + ('system', 'routerboard', ): [ + { + 'routerboard': True, + 'model': '750GL', + 'serial-number': '0123456789AB', + 'firmware-type': 'ar7240', + 'factory-firmware': '3.09', + 'current-firmware': '6.49.6', + 'upgrade-firmware': '6.49.6', + }, + ], + ('routing', 'bgp', 'peer', ): [], + ('routing', 'bgp', 'vpnv4-route', ): [], + ('routing', 'bgp', 'instance', ): [ + { + '.id': '*0', + 'name': 'default', + 'as': 65530, + 'router-id': '0.0.0.0', + 'redistribute-connected': False, + 'redistribute-static': False, + 'redistribute-rip': False, + 'redistribute-ospf': False, + 'redistribute-other-bgp': False, + 'out-filter': '', + 'client-to-client-reflection': True, + 'ignore-as-path-len': False, + 'routing-table': '', + 'default': True, + 'disabled': False, + }, + ], + ('ip', 'route', ): [ + { + '.id': '*30000001', + 'dst-address': '0.0.0.0/0', + 'gateway': '1.2.3.0', + 'gateway-status': '1.2.3.0 reachable via first-ether', + 'distance': 1, + 'scope': 30, + 'target-scope': 10, + 'vrf-interface': 'first-ether', + 'active': True, + 'dynamic': True, + 'static': True, + 'disabled': False, + }, + { + '.id': '*40162F13', + 'dst-address': '84.73.216.0/21', + 'pref-src': '1.2.3.4', + 'gateway': 'first-ether', + 'gateway-status': 'first-ether reachable', + 'distance': 0, + 'scope': 10, + 'active': True, + 'dynamic': True, + 'connect': True, + 'disabled': False, + }, + { + '.id': '*4016AA23', + 'dst-address': '192.168.2.0/24', + 'pref-src': '192.168.2.1', + 'gateway': 'fourth-ether', + 'gateway-status': 'fourth-ether reachable', + 'distance': 0, + 'scope': 10, + 'active': True, + 'dynamic': True, + 'connect': True, + 'disabled': False, + }, + { + '.id': '*40168E05', + 'dst-address': '192.168.1.0/24', + 'pref-src': '192.168.1.1', + 'gateway': 'my-bridge', + 'gateway-status': 'my-bridge reachable', + 'distance': 0, + 'scope': 10, + 'active': True, + 'dynamic': True, + 'connect': True, + 'disabled': False, + }, + ], + ('routing', 'ospf', 'instance', ): [ + { + '.id': '*0', + 'name': 'default', + 'router-id': '0.0.0.0', + 'distribute-default': 'never', + 'redistribute-connected': False, + 'redistribute-static': False, + 'redistribute-rip': False, + 'redistribute-bgp': False, + 'redistribute-other-ospf': False, + 'metric-default': 1, + 'metric-connected': 20, + 'metric-static': 20, + 'metric-rip': 20, + 'metric-bgp': 'auto', + 'metric-other-ospf': 'auto', + 'in-filter': 'ospf-in', + 'out-filter': 'ospf-out', + 'state': 'down', + 'default': True, + 'disabled': False, + }, + ], + ('routing', 'ospf', 'neighbor', ): [], +} + + +class TestRouterosApiFactsModule(ModuleTestCase): + + def setUp(self): + super(TestRouterosApiFactsModule, self).setUp() + self.module = api_facts + self.module.LibRouterosError = FakeLibRouterosError + self.module.connect = MagicMock(new=fake_ros_api) + self.module.check_has_library = MagicMock() + self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api_facts.create_api', MagicMock(new=fake_ros_api)) + self.patch_create_api.start() + self.patch_query_path = patch('ansible_collections.community.routeros.plugins.modules.api_facts.FactsBase.query_path', self.query_path) + self.patch_query_path.start() + self.module.Key = MagicMock(new=Key) + self.config_module_args = { + 'username': 'admin', + 'password': 'pаss', + 'hostname': '127.0.0.1', + } + + def tearDown(self): + self.patch_query_path.stop() + self.patch_create_api.stop() + + def query_path(self, path): + response = API_RESPONSES.get(tuple(path)) + if response is None: + raise Exception('Unexpected command: %s' % repr(path)) + return response + + def test_module_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson) as exc: + with set_module_args({}): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + + def test_module_fail_when_invalid_gather_subset(self): + with self.assertRaises(AnsibleFailJson) as exc: + module_args = self.config_module_args.copy() + module_args['gather_subset'] = ['!foobar'] + with set_module_args(module_args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Bad subset: foobar') + + def test_full_run(self): + with self.assertRaises(AnsibleExitJson) as exc: + with set_module_args(self.config_module_args.copy()): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['ansible_facts']['ansible_net_all_ipv4_addresses'], [ + '192.168.1.1', + '192.168.2.1', + '1.2.3.4', + ]) + self.assertEqual(result['ansible_facts']['ansible_net_all_ipv6_addresses'], [ + 'fe80::1:2:3', + 'fe80::1:2:4', + 'fe80::1:2:5', + ]) + self.assertEqual(result['ansible_facts']['ansible_net_arch'], 'mipsbe') + self.assertEqual(result['ansible_facts']['ansible_net_bgp_instance'], { + 'default': { + 'as': 65530, + 'client-to-client-reflection': True, + 'default': True, + 'disabled': False, + 'ignore-as-path-len': False, + 'name': 'default', + 'out-filter': '', + 'redistribute-connected': False, + 'redistribute-ospf': False, + 'redistribute-other-bgp': False, + 'redistribute-rip': False, + 'redistribute-static': False, + 'router-id': '0.0.0.0', + 'routing-table': '' + }, + }) + self.assertEqual(result['ansible_facts']['ansible_net_bgp_peer'], {}) + self.assertEqual(result['ansible_facts']['ansible_net_bgp_vpnv4_route'], {}) + self.assertEqual(result['ansible_facts']['ansible_net_cpu_load'], 48) + self.assertEqual(result['ansible_facts']['ansible_net_gather_subset'], [ + 'default', + 'hardware', + 'interfaces', + 'routing', + ]) + self.assertEqual(result['ansible_facts']['ansible_net_hostname'], 'MikroTik') + self.assertEqual(result['ansible_facts']['ansible_net_interfaces'], { + 'my-bridge': { + 'actual-mtu': 1500, + 'disabled': False, + 'fp-rx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-byte': 1234, + 'fp-tx-packet': 1234, + 'ipv4': [ + { + 'address': '192.168.1.1', + 'subnet': 24 + } + ], + 'ipv6': [ + { + 'address': 'fe80::1:2:3', + 'subnet': 64 + } + ], + 'l2mtu': 1598, + 'last-link-up-time': 'apr/22/2022 07:54:48', + 'link-downs': 0, + 'mac-address': '00:11:22:33:44:66', + 'mtu': 'auto', + 'name': 'my-bridge', + 'running': True, + 'rx-byte': 1234, + 'rx-drop': 1234, + 'rx-error': 1234, + 'rx-packet': 1234, + 'tx-byte': 1234, + 'tx-drop': 1234, + 'tx-error': 1234, + 'tx-packet': 1234, + 'tx-queue-drop': 1234, + 'type': 'bridge' + }, + 'first-ether': { + 'actual-mtu': 1500, + 'default-name': 'ether1', + 'disabled': False, + 'fp-rx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-byte': 1234, + 'fp-tx-packet': 1234, + 'ipv4': [ + { + 'address': '1.2.3.4', + 'subnet': 21 + } + ], + 'ipv6': [ + { + 'address': 'fe80::1:2:5', + 'subnet': 64 + } + ], + 'l2mtu': 1598, + 'last-link-up-time': 'apr/22/2022 07:54:55', + 'link-downs': 0, + 'mac-address': '00:11:22:33:44:55', + 'max-l2mtu': 4074, + 'mtu': 1500, + 'name': 'first-ether', + 'running': True, + 'rx-byte': 1234, + 'rx-drop': 1234, + 'rx-error': 1234, + 'rx-packet': 1234, + 'tx-byte': 1234, + 'tx-drop': 1234, + 'tx-error': 1234, + 'tx-packet': 1234, + 'tx-queue-drop': 1234, + 'type': 'ether' + }, + 'second-ether': { + 'actual-mtu': 1500, + 'default-name': 'ether2', + 'disabled': False, + 'fp-rx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-byte': 1234, + 'fp-tx-packet': 1234, + 'l2mtu': 1598, + 'last-link-up-time': 'apr/22/2022 07:54:55', + 'link-downs': 0, + 'mac-address': '00:11:22:33:44:66', + 'max-l2mtu': 4074, + 'mtu': 1500, + 'name': 'second-ether', + 'running': True, + 'rx-byte': 1234, + 'rx-drop': 1234, + 'rx-error': 1234, + 'rx-packet': 1234, + 'slave': True, + 'tx-byte': 1234, + 'tx-drop': 1234, + 'tx-error': 1234, + 'tx-packet': 1234, + 'tx-queue-drop': 1234, + 'type': 'ether' + }, + 'third-ether': { + 'actual-mtu': 1500, + 'default-name': 'ether3', + 'disabled': False, + 'fp-rx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-byte': 1234, + 'fp-tx-packet': 1234, + 'l2mtu': 1598, + 'last-link-up-time': 'apr/22/2022 07:54:55', + 'link-downs': 0, + 'mac-address': '00:11:22:33:44:77', + 'max-l2mtu': 4074, + 'mtu': 1500, + 'name': 'third-ether', + 'running': True, + 'rx-byte': 1234, + 'rx-drop': 1234, + 'rx-error': 1234, + 'rx-packet': 1234, + 'slave': True, + 'tx-byte': 1234, + 'tx-drop': 1234, + 'tx-error': 1234, + 'tx-packet': 1234, + 'tx-queue-drop': 1234, + 'type': 'ether' + }, + 'fourth-ether': { + 'actual-mtu': 1500, + 'default-name': 'ether4', + 'disabled': False, + 'fp-rx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-byte': 1234, + 'fp-tx-packet': 1234, + 'ipv4': [ + { + 'address': '192.168.2.1', + 'subnet': 24 + } + ], + 'ipv6': [ + { + 'address': 'fe80::1:2:4', + 'subnet': 64 + } + ], + 'l2mtu': 1598, + 'last-link-down-time': 'apr/23/2022 08:22:50', + 'last-link-up-time': 'apr/23/2022 08:22:52', + 'link-downs': 2, + 'mac-address': '00:11:22:33:44:88', + 'max-l2mtu': 4074, + 'mtu': 1500, + 'name': 'fourth-ether', + 'running': True, + 'rx-byte': 1234, + 'rx-drop': 1234, + 'rx-error': 1234, + 'rx-packet': 1234, + 'tx-byte': 1234, + 'tx-drop': 1234, + 'tx-error': 1234, + 'tx-packet': 1234, + 'tx-queue-drop': 1234, + 'type': 'ether' + }, + 'fifth-ether': { + 'actual-mtu': 1500, + 'default-name': 'ether5', + 'disabled': False, + 'fp-rx-byte': 1234, + 'fp-rx-packet': 1234, + 'fp-tx-byte': 1234, + 'fp-tx-packet': 1234, + 'l2mtu': 1598, + 'last-link-down-time': 'may/02/2022 18:12:32', + 'last-link-up-time': 'may/02/2022 18:08:01', + 'link-downs': 14, + 'mac-address': '00:11:22:33:44:99', + 'max-l2mtu': 4074, + 'mtu': 1500, + 'name': 'fifth-ether', + 'running': False, + 'rx-byte': 1234, + 'rx-drop': 1234, + 'rx-error': 1234, + 'rx-packet': 1234, + 'slave': True, + 'tx-byte': 1234, + 'tx-drop': 1234, + 'tx-error': 1234, + 'tx-packet': 1234, + 'tx-queue-drop': 1234, + 'type': 'ether' + } + }) + self.assertEqual(result['ansible_facts']['ansible_net_memfree_mb'], 12345678 / 1048576.0) + self.assertEqual(result['ansible_facts']['ansible_net_memtotal_mb'], 23456789 / 1048576.0) + self.assertEqual(result['ansible_facts']['ansible_net_model'], '750GL') + self.assertEqual(result['ansible_facts']['ansible_net_neighbors'], []) + self.assertEqual(result['ansible_facts']['ansible_net_ospf_instance'], { + 'default': { + 'default': True, + 'disabled': False, + 'distribute-default': 'never', + 'in-filter': 'ospf-in', + 'metric-bgp': 'auto', + 'metric-connected': 20, + 'metric-default': 1, + 'metric-other-ospf': 'auto', + 'metric-rip': 20, + 'metric-static': 20, + 'name': 'default', + 'out-filter': 'ospf-out', + 'redistribute-bgp': False, + 'redistribute-connected': False, + 'redistribute-other-ospf': False, + 'redistribute-rip': False, + 'redistribute-static': False, + 'router-id': '0.0.0.0', + 'state': 'down' + } + }) + self.assertEqual(result['ansible_facts']['ansible_net_ospf_neighbor'], {}) + self.assertEqual(result['ansible_facts']['ansible_net_route'], { + 'main': { + 'active': True, + 'connect': True, + 'disabled': False, + 'distance': 0, + 'dst-address': '192.168.1.0/24', + 'dynamic': True, + 'gateway': 'my-bridge', + 'gateway-status': 'my-bridge reachable', + 'pref-src': '192.168.1.1', + 'scope': 10 + } + }) + self.assertEqual(result['ansible_facts']['ansible_net_serialnum'], '0123456789AB') + self.assertEqual(result['ansible_facts']['ansible_net_spacefree_mb'], 123456789 / 1048576.0) + self.assertEqual(result['ansible_facts']['ansible_net_spacetotal_mb'], 234567890 / 1048576.0) + self.assertEqual(result['ansible_facts']['ansible_net_uptime'], '2w3d4h5m6s') + self.assertEqual(result['ansible_facts']['ansible_net_version'], '6.49.6 (stable)') diff --git a/tests/unit/plugins/modules/test_api_find_and_modify.py b/tests/unit/plugins/modules/test_api_find_and_modify.py new file mode 100644 index 0000000..e700f9d --- /dev/null +++ b/tests/unit/plugins/modules/test_api_find_and_modify.py @@ -0,0 +1,759 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch, MagicMock +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase + +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import ( + FakeLibRouterosError, fake_ros_api, massage_expected_result_data, create_fake_path, +) +from ansible_collections.community.routeros.plugins.modules import api_find_and_modify + + +START_IP_DNS_STATIC = [ + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'dynamic': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'dynamic': False, + }, + { + '.id': '*7', + 'comment': '', + 'name': 'foo', + 'address': '192.168.88.2', + 'dynamic': False, + }, +] + +START_IP_DNS_STATIC_OLD_DATA = massage_expected_result_data(START_IP_DNS_STATIC, ('ip', 'dns', 'static'), keep_all=True) + +START_IP_FIREWALL_FILTER = [ + { + '.id': '*2', + 'action': 'accept', + 'chain': 'input', + 'comment': 'defconf', + 'protocol': 'icmp', + }, + { + '.id': '*3', + 'action': 'accept', + 'chain': 'input', + 'comment': 'defconf', + 'connection-state': 'established', + }, + { + '.id': '*4', + 'action': 'accept', + 'chain': 'input', + 'comment': 'defconf', + 'connection-state': 'related', + }, + { + '.id': '*7', + 'action': 'drop', + 'chain': 'input', + 'comment': 'defconf', + 'in-interface': 'wan', + }, + { + '.id': '*8', + 'action': 'accept', + 'chain': 'forward', + 'comment': 'defconf', + 'connection-state': 'established', + }, + { + '.id': '*9', + 'action': 'accept', + 'chain': 'forward', + 'comment': 'defconf', + 'connection-state': 'related', + }, + { + '.id': '*A', + 'action': 'drop', + 'chain': 'forward', + 'comment': 'defconf', + 'connection-status': 'invalid', + }, +] + +START_IP_FIREWALL_FILTER_OLD_DATA = massage_expected_result_data(START_IP_FIREWALL_FILTER, ('ip', 'firewall', 'filter'), keep_all=True) + +START_IP_SERVICE = [ + # I removed all entryes not for 'api' and 'api-ssl' + { + "certificate": None, + "tls-version": None, + ".id": "*7", + "address": "", + "disabled": True, + "dynamic": False, + "invalid": True, + "name": "api", + "port": 8728, + "proto": "tcp", + "vrf": "main" + }, + { + ".id": "*9", + "address": "192.168.1.0/24", + "certificate": "mycert", + "dynamic": False, + "invalid": False, + "name": "api-ssl", + "port": 8729, + "proto": "tcp", + "tls-version": "only-1.2", + "vrf": "main" + }, + { + "address": None, + "certificate": None, + "max-sessions": None, + "tls-version": None, + ".id": "*13", + "connection": True, + "dynamic": True, + "invalid": False, + "local": "192.168.1.1", + "name": "api-ssl", + "port": 8729, + "proto": "tcp", + "remote": "192.168.1.2:12346" + } +] + +START_IP_SERVICE_OLD_DATA = massage_expected_result_data(START_IP_SERVICE, ('ip', 'service'), keep_all=True) + + +class TestRouterosApiFindAndModifyModule(ModuleTestCase): + + def setUp(self): + super(TestRouterosApiFindAndModifyModule, self).setUp() + self.module = api_find_and_modify + self.module.LibRouterosError = FakeLibRouterosError + self.module.connect = MagicMock(new=fake_ros_api) + self.module.check_has_library = MagicMock() + self.patch_create_api = patch( + 'ansible_collections.community.routeros.plugins.modules.api_find_and_modify.create_api', + MagicMock(new=fake_ros_api)) + self.patch_create_api.start() + self.config_module_args = { + 'username': 'admin', + 'password': 'pаss', + 'hostname': '127.0.0.1', + } + + def tearDown(self): + self.patch_create_api.stop() + + def test_module_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson) as exc: + with set_module_args({}): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + + def test_invalid_disabled_and_enabled_option_in_find(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + 'comment': 'foo', + '!comment': None, + }, + 'values': { + 'comment': 'bar', + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], '`find` must not contain both "comment" and "!comment"!') + + def test_invalid_disabled_option_invalid_value_in_find(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + '!comment': 'gone', + }, + 'values': { + 'comment': 'bar', + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'The value for "!comment" in `find` must not be non-trivial!') + + def test_invalid_disabled_and_enabled_option_in_values(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': {}, + 'values': { + 'comment': 'foo', + '!comment': None, + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], '`values` must not contain both "comment" and "!comment"!') + + def test_invalid_disabled_option_invalid_value_in_values(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': {}, + 'values': { + '!comment': 'gone', + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'The value for "!comment" in `values` must not be non-trivial!') + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_change_invalid_zero(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + 'name': 'bam', + }, + 'values': { + 'name': 'baz', + }, + 'require_matches_min': 10, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Found no entries, but allow_no_matches=false') + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_change_invalid_too_few(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + 'name': 'router', + }, + 'values': { + 'name': 'foobar', + }, + 'require_matches_min': 10, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Found 2 entries, but expected at least 10') + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_change_invalid_too_many(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + 'name': 'router', + }, + 'values': { + 'name': 'foobar', + }, + 'require_matches_max': 1, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Found 2 entries, but expected at most 1') + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_change_idempotent_zero_matches_1(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + 'name': 'baz', + }, + 'values': { + 'name': 'bam', + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['match_count'], 0) + self.assertEqual(result['modify_count'], 0) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_change_idempotent_zero_matches_2(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + 'name': 'baz', + }, + 'values': { + 'name': 'bam', + }, + 'require_matches_min': 2, + 'allow_no_matches': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['match_count'], 0) + self.assertEqual(result['modify_count'], 0) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_idempotent_1(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + }, + 'values': { + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['match_count'], 3) + self.assertEqual(result['modify_count'], 0) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_idempotent_2(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + 'name': 'foo', + }, + 'values': { + 'comment': None, + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['match_count'], 1) + self.assertEqual(result['modify_count'], 0) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_change(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + 'name': 'foo', + }, + 'values': { + 'comment': 'bar', + }, + '_ansible_diff': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'comment': 'bar', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + ]) + self.assertEqual(result['diff']['before']['values'], [ + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + ]) + self.assertEqual(result['diff']['after']['values'], [ + { + '.id': '*7', + 'comment': 'bar', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + ]) + self.assertEqual(result['match_count'], 1) + self.assertEqual(result['modify_count'], 1) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_change_remove_comment_1(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + }, + 'values': { + 'comment': None, + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + ]) + self.assertEqual('diff' in result, False) + self.assertEqual(result['match_count'], 3) + self.assertEqual(result['modify_count'], 1) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_change_remove_comment_2(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + }, + 'values': { + 'comment': '', + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + ]) + self.assertEqual(result['match_count'], 3) + self.assertEqual(result['modify_count'], 1) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_change_remove_comment_3(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'find': { + }, + 'values': { + '!comment': None, + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + 'match-subdomain': False, + }, + ]) + self.assertEqual(result['match_count'], 3) + self.assertEqual(result['modify_count'], 1) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'firewall', 'filter'), START_IP_FIREWALL_FILTER)) + def test_change_remove_generic(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip firewall filter', + 'find': { + 'chain': 'input', + '!protocol': '', + }, + 'values': { + '!connection-state': None, + }, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_FIREWALL_FILTER_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*2', + 'action': 'accept', + 'chain': 'input', + 'comment': 'defconf', + 'protocol': 'icmp', + 'disabled': False, + 'log': False, + 'log-prefix': '', + }, + { + '.id': '*3', + 'action': 'accept', + 'chain': 'input', + 'comment': 'defconf', + 'disabled': False, + 'log': False, + 'log-prefix': '', + }, + { + '.id': '*4', + 'action': 'accept', + 'chain': 'input', + 'comment': 'defconf', + 'disabled': False, + 'log': False, + 'log-prefix': '', + }, + { + '.id': '*7', + 'action': 'drop', + 'chain': 'input', + 'comment': 'defconf', + 'disabled': False, + 'in-interface': 'wan', + 'log': False, + 'log-prefix': '', + }, + { + '.id': '*8', + 'action': 'accept', + 'chain': 'forward', + 'comment': 'defconf', + 'connection-state': 'established', + 'disabled': False, + 'log': False, + 'log-prefix': '', + }, + { + '.id': '*9', + 'action': 'accept', + 'chain': 'forward', + 'comment': 'defconf', + 'connection-state': 'related', + 'disabled': False, + 'log': False, + 'log-prefix': '', + }, + { + '.id': '*A', + 'action': 'drop', + 'chain': 'forward', + 'comment': 'defconf', + 'connection-status': 'invalid', + 'disabled': False, + 'log': False, + 'log-prefix': '', + }, + ]) + self.assertEqual(result['match_count'], 3) + self.assertEqual(result['modify_count'], 2) + + @patch('ansible_collections.community.routeros.plugins.modules.api_find_and_modify.compose_api_path', + new=create_fake_path(('ip', 'service'), START_IP_SERVICE)) + def test_change_ignore_dynamic(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip service', + 'find': { + 'name': 'api-ssl', + }, + 'values': { + 'address': '192.168.1.0/24', + }, + 'ignore_dynamic': True, + '_ansible_diff': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], [entry for entry in START_IP_SERVICE_OLD_DATA if entry["dynamic"] is False]) + self.assertEqual(result['new_data'], [entry for entry in START_IP_SERVICE_OLD_DATA if entry["dynamic"] is False]) + self.assertEqual(result['match_count'], 1) + self.assertEqual(result['modify_count'], 0) diff --git a/tests/unit/plugins/modules/test_api_info.py b/tests/unit/plugins/modules/test_api_info.py new file mode 100644 index 0000000..0fac95a --- /dev/null +++ b/tests/unit/plugins/modules/test_api_info.py @@ -0,0 +1,983 @@ +# Copyright (c) 2022, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch, MagicMock +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase + +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import ( + FAKE_ROS_VERSION, FakeLibRouterosError, Key, fake_ros_api, +) +from ansible_collections.community.routeros.plugins.modules import api_info + + +class TestRouterosApiInfoModule(ModuleTestCase): + + def setUp(self): + super(TestRouterosApiInfoModule, self).setUp() + self.module = api_info + self.module.LibRouterosError = FakeLibRouterosError + self.module.connect = MagicMock(new=fake_ros_api) + self.module.check_has_library = MagicMock() + self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api_info.create_api', MagicMock(new=fake_ros_api)) + self.patch_create_api.start() + self.patch_get_api_version = patch( + 'ansible_collections.community.routeros.plugins.modules.api_info.get_api_version', + MagicMock(return_value=FAKE_ROS_VERSION)) + self.patch_get_api_version.start() + self.module.Key = MagicMock(new=Key) + self.config_module_args = { + 'username': 'admin', + 'password': 'pаss', + 'hostname': '127.0.0.1', + } + + def tearDown(self): + self.patch_get_api_version.stop() + self.patch_create_api.stop() + + def test_module_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson) as exc: + with set_module_args({}): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + + def test_invalid_path(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'something invalid' + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'].startswith('value of path must be one of: '), True) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_empty_result(self, mock_compose_api_path): + mock_compose_api_path.return_value = [] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static' + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], []) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_regular_result(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'called-format': 'mac:ssid', + 'interim-update': 'enabled', + 'mac-caching': 'disabled', + 'mac-format': 'XX:XX:XX:XX:XX:XX', + 'mac-mode': 'as-username', + 'foo': 'bar', + '.id': '*1', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'caps-man aaa', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [{ + 'interim-update': 'enabled', + '.id': '*1', + }]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_result_with_defaults(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'called-format': 'mac:ssid', + 'interim-update': 'enabled', + 'mac-caching': 'disabled', + 'mac-format': 'XX:XX:XX:XX:XX:XX', + 'mac-mode': 'as-username', + 'foo': 'bar', + '.id': '*1', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'caps-man aaa', + 'hide_defaults': False, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [{ + 'called-format': 'mac:ssid', + 'interim-update': 'enabled', + 'mac-caching': 'disabled', + 'mac-format': 'XX:XX:XX:XX:XX:XX', + 'mac-mode': 'as-username', + '.id': '*1', + }]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_full_result(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'called-format': 'mac:ssid', + 'interim-update': 'enabled', + 'mac-caching': 'disabled', + 'mac-format': 'XX:XX:XX:XX:XX:XX', + 'mac-mode': 'as-username', + 'foo': 'bar', + '.id': '*1', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'caps-man aaa', + 'unfiltered': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [{ + 'interim-update': 'enabled', + 'foo': 'bar', + '.id': '*1', + }]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_disabled_exclamation(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + '.id': '*1', + 'dynamic': False, + }, + { + 'chain': 'forward', + 'action': 'drop', + 'in-interface': 'sfp1', + '.id': '*2', + 'dynamic': True, + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip firewall filter', + 'handle_disabled': 'exclamation', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [{ + 'chain': 'input', + 'in-interface-list': 'LAN', + '!action': None, + '!address-list': None, + '!address-list-timeout': None, + '!comment': None, + '!connection-bytes': None, + '!connection-limit': None, + '!connection-mark': None, + '!connection-nat-state': None, + '!connection-rate': None, + '!connection-state': None, + '!connection-type': None, + '!content': None, + '!disabled': None, + '!dscp': None, + '!dst-address': None, + '!dst-address-list': None, + '!dst-address-type': None, + '!dst-limit': None, + '!dst-port': None, + '!fragment': None, + '!hotspot': None, + '!hw-offload': None, + '!icmp-options': None, + '!in-bridge-port': None, + '!in-bridge-port-list': None, + '!in-interface': None, + '!ingress-priority': None, + '!ipsec-policy': None, + '!ipv4-options': None, + '!jump-target': None, + '!layer7-protocol': None, + '!limit': None, + '!log': None, + '!log-prefix': None, + '!nth': None, + '!out-bridge-port': None, + '!out-bridge-port-list': None, + '!out-interface': None, + '!out-interface-list': None, + '!p2p': None, + '!packet-mark': None, + '!packet-size': None, + '!per-connection-classifier': None, + '!port': None, + '!priority': None, + '!protocol': None, + '!psd': None, + '!random': None, + '!realm': None, + '!reject-with': None, + '!routing-mark': None, + '!routing-table': None, + '!src-address': None, + '!src-address-list': None, + '!src-address-type': None, + '!src-mac-address': None, + '!src-port': None, + '!tcp-flags': None, + '!tcp-mss': None, + '!time': None, + '!tls-host': None, + '!ttl': None, + '.id': '*1', + }]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_disabled_null_value(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + '.id': '*1', + 'dynamic': False, + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip firewall filter', + 'handle_disabled': 'null-value', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [{ + 'chain': 'input', + 'in-interface-list': 'LAN', + 'action': None, + 'address-list': None, + 'address-list-timeout': None, + 'comment': None, + 'connection-bytes': None, + 'connection-limit': None, + 'connection-mark': None, + 'connection-nat-state': None, + 'connection-rate': None, + 'connection-state': None, + 'connection-type': None, + 'content': None, + 'disabled': None, + 'dscp': None, + 'dst-address': None, + 'dst-address-list': None, + 'dst-address-type': None, + 'dst-limit': None, + 'dst-port': None, + 'fragment': None, + 'hotspot': None, + 'hw-offload': None, + 'icmp-options': None, + 'in-bridge-port': None, + 'in-bridge-port-list': None, + 'in-interface': None, + 'ingress-priority': None, + 'ipsec-policy': None, + 'ipv4-options': None, + 'jump-target': None, + 'layer7-protocol': None, + 'limit': None, + 'log': None, + 'log-prefix': None, + 'nth': None, + 'out-bridge-port': None, + 'out-bridge-port-list': None, + 'out-interface': None, + 'out-interface-list': None, + 'p2p': None, + 'packet-mark': None, + 'packet-size': None, + 'per-connection-classifier': None, + 'port': None, + 'priority': None, + 'protocol': None, + 'psd': None, + 'random': None, + 'realm': None, + 'reject-with': None, + 'routing-mark': None, + 'routing-table': None, + 'src-address': None, + 'src-address-list': None, + 'src-address-type': None, + 'src-mac-address': None, + 'src-port': None, + 'tcp-flags': None, + 'tcp-mss': None, + 'time': None, + 'tls-host': None, + 'ttl': None, + '.id': '*1', + }]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_disabled_omit(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + '.id': '*1', + 'dynamic': False, + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip firewall filter', + 'handle_disabled': 'omit', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [{ + 'chain': 'input', + 'in-interface-list': 'LAN', + '.id': '*1', + }]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_dynamic(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + 'dynamic': False, + '.id': '*1', + }, + { + 'chain': 'forward', + 'action': 'drop', + 'in-interface': 'sfp1', + '.id': '*2', + 'dynamic': True, + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip firewall filter', + 'handle_disabled': 'omit', + 'include_dynamic': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + '.id': '*1', + 'dynamic': False, + }, + { + 'chain': 'forward', + 'action': 'drop', + 'in-interface': 'sfp1', + '.id': '*2', + 'dynamic': True, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_builtin_exclude(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + '.id': '*2000000', + 'name': 'all', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': True, + 'comment': 'contains all interfaces', + }, + { + '.id': '*2000001', + 'name': 'none', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': True, + 'comment': 'contains no interfaces', + }, + { + '.id': '*2000010', + 'name': 'WAN', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': False, + 'comment': 'defconf', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'interface list', + 'handle_disabled': 'omit', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + '.id': '*2000010', + 'name': 'WAN', + 'comment': 'defconf', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_builtin_include(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + '.id': '*2000000', + 'name': 'all', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': True, + 'comment': 'contains all interfaces', + }, + { + '.id': '*2000001', + 'name': 'none', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': True, + 'comment': 'contains no interfaces', + }, + { + '.id': '*2000010', + 'name': 'WAN', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': False, + 'comment': 'defconf', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'interface list', + 'handle_disabled': 'omit', + 'include_builtin': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + '.id': '*2000000', + 'name': 'all', + 'builtin': True, + 'comment': 'contains all interfaces', + }, + { + '.id': '*2000001', + 'name': 'none', + 'builtin': True, + 'comment': 'contains no interfaces', + }, + { + '.id': '*2000010', + 'name': 'WAN', + 'builtin': False, + 'comment': 'defconf', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_absent(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + '.id': '*1', + 'address': '192.168.88.2', + 'mac-address': '11:22:33:44:55:66', + 'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2', + 'address-lists': '', + 'server': 'main', + 'dhcp-option': '', + 'status': 'waiting', + 'last-seen': 'never', + 'radius': False, + 'dynamic': False, + 'blocked': False, + 'disabled': False, + 'comment': 'foo', + }, + { + '.id': '*2', + 'address': '192.168.88.3', + 'mac-address': '11:22:33:44:55:77', + 'client-id': '1:2:3:4:5:6:7', + 'address-lists': '', + 'server': 'main', + 'dhcp-option': '', + 'status': 'bound', + 'expires-after': '3d7m8s', + 'last-seen': '1m52s', + 'active-address': '192.168.88.14', + 'active-mac-address': '11:22:33:44:55:76', + 'active-client-id': '1:2:3:4:5:6:7', + 'active-server': 'main', + 'host-name': 'bar', + 'radius': False, + 'dynamic': False, + 'blocked': False, + 'disabled': False, + }, + { + '.id': '*3', + 'address': '0.0.0.1', + 'mac-address': '00:00:00:00:00:01', + 'address-lists': '', + 'dhcp-option': '', + 'status': 'waiting', + 'last-seen': 'never', + 'radius': False, + 'dynamic': False, + 'blocked': False, + 'disabled': False, + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dhcp-server lease', + 'handle_disabled': 'omit', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + '.id': '*1', + 'address': '192.168.88.2', + 'mac-address': '11:22:33:44:55:66', + 'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2', + 'server': 'main', + 'comment': 'foo', + }, + { + '.id': '*2', + 'address': '192.168.88.3', + 'mac-address': '11:22:33:44:55:77', + 'client-id': '1:2:3:4:5:6:7', + 'server': 'main', + }, + { + '.id': '*3', + 'address': '0.0.0.1', + 'mac-address': '00:00:00:00:00:01', + 'server': 'all', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_default_disable_1(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + '.id': '*10', + 'name': 'gre-tunnel3', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '0.0.0.0', + 'remote-address': '192.168.1.1', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + }, + { + '.id': '*11', + 'name': 'gre-tunnel4', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '0.0.0.0', + 'remote-address': '192.168.1.2', + 'keepalive': '10s,10', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + }, + { + '.id': '*12', + 'name': 'gre-tunnel5', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '192.168.0.1', + 'remote-address': '192.168.1.3', + 'keepalive': '20s,20', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + 'comment': 'foo', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'interface gre', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + '.id': '*10', + 'name': 'gre-tunnel3', + 'remote-address': '192.168.1.1', + '!comment': None, + '!ipsec-secret': None, + '!keepalive': None, + }, + { + '.id': '*11', + 'name': 'gre-tunnel4', + 'remote-address': '192.168.1.2', + '!comment': None, + '!ipsec-secret': None, + }, + { + '.id': '*12', + 'name': 'gre-tunnel5', + 'local-address': '192.168.0.1', + 'remote-address': '192.168.1.3', + 'keepalive': '20s,20', + 'comment': 'foo', + '!ipsec-secret': None, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_default_disable_2(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + '.id': '*10', + 'name': 'gre-tunnel3', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '0.0.0.0', + 'remote-address': '192.168.1.1', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + }, + { + '.id': '*11', + 'name': 'gre-tunnel4', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '0.0.0.0', + 'remote-address': '192.168.1.2', + 'keepalive': '10s,10', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + }, + { + '.id': '*12', + 'name': 'gre-tunnel5', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '192.168.0.1', + 'remote-address': '192.168.1.3', + 'keepalive': '20s,20', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + 'comment': 'foo', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'interface gre', + 'handle_disabled': 'omit', + 'hide_defaults': False, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + '.id': '*10', + 'name': 'gre-tunnel3', + 'mtu': 'auto', + 'local-address': '0.0.0.0', + 'remote-address': '192.168.1.1', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'disabled': False, + }, + { + '.id': '*11', + 'name': 'gre-tunnel4', + 'mtu': 'auto', + 'local-address': '0.0.0.0', + 'remote-address': '192.168.1.2', + 'keepalive': '10s,10', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'disabled': False, + }, + { + '.id': '*12', + 'name': 'gre-tunnel5', + 'mtu': 'auto', + 'local-address': '192.168.0.1', + 'remote-address': '192.168.1.3', + 'keepalive': '20s,20', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'disabled': False, + 'comment': 'foo', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_restrict_1(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + 'dynamic': False, + '.id': '*1', + }, + { + 'chain': 'forward', + 'action': 'drop', + 'in-interface': 'sfp1', + '.id': '*2', + 'dynamic': False, + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip firewall filter', + 'handle_disabled': 'omit', + 'restrict': [], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + '.id': '*1', + }, + { + 'chain': 'forward', + 'action': 'drop', + 'in-interface': 'sfp1', + '.id': '*2', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_restrict_2(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + 'dynamic': False, + '.id': '*1', + }, + { + 'chain': 'forward', + 'action': 'drop', + 'in-interface': 'sfp1', + '.id': '*2', + 'dynamic': False, + }, + { + 'chain': 'input', + 'action': 'drop', + 'dynamic': False, + '.id': '*3', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip firewall filter', + 'handle_disabled': 'omit', + 'restrict': [{ + 'field': 'chain', + 'values': ['forward'], + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + 'chain': 'forward', + 'action': 'drop', + 'in-interface': 'sfp1', + '.id': '*2', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_info.compose_api_path') + def test_restrict_3(self, mock_compose_api_path): + mock_compose_api_path.return_value = [ + { + 'chain': 'input', + 'in-interface-list': 'LAN', + 'dynamic': False, + '.id': '*1', + }, + { + 'chain': 'forward', + 'action': 'drop', + 'in-interface': 'sfp1', + '.id': '*2', + 'dynamic': False, + }, + { + 'chain': 'input', + 'action': 'drop', + 'dynamic': False, + '.id': '*3', + }, + { + 'chain': 'input', + 'action': 'drop', + 'comment': 'Foo', + 'dynamic': False, + '.id': '*4', + }, + { + 'chain': 'input', + 'action': 'drop', + 'comment': 'Bar', + 'dynamic': False, + '.id': '*5', + }, + ] + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip firewall filter', + 'handle_disabled': 'omit', + 'restrict': [ + { + 'field': 'chain', + 'values': ['input', 'foobar'], + }, + { + 'field': 'action', + 'values': ['drop', 42], + }, + { + 'field': 'comment', + 'values': [None, 'Foo'], + }, + ], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['result'], [ + { + 'chain': 'input', + 'action': 'drop', + '.id': '*3', + }, + { + 'chain': 'input', + 'action': 'drop', + 'comment': 'Foo', + '.id': '*4', + }, + ]) diff --git a/tests/unit/plugins/modules/test_api_modify.py b/tests/unit/plugins/modules/test_api_modify.py new file mode 100644 index 0000000..6edc046 --- /dev/null +++ b/tests/unit/plugins/modules/test_api_modify.py @@ -0,0 +1,2015 @@ +# Copyright (c) 2022, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch, MagicMock +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase + +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import ( + FAKE_ROS_VERSION, FakeLibRouterosError, fake_ros_api, massage_expected_result_data, create_fake_path, +) +from ansible_collections.community.routeros.plugins.modules import api_modify + + +START_IP_DNS_STATIC = [ + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'dynamic': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'dynamic': False, + }, + { + '.id': '*7', + 'comment': '', + 'name': 'foo', + 'address': '192.168.88.2', + 'dynamic': False, + }, + { + '.id': '*8', + 'comment': '', + 'name': 'dynfoo', + 'address': '192.168.88.15', + 'dynamic': True, + }, +] + +START_IP_DNS_STATIC_OLD_DATA = massage_expected_result_data(START_IP_DNS_STATIC, ('ip', 'dns', 'static'), remove_dynamic=True) + +START_IP_SETTINGS = [ + { + 'accept-redirects': True, + 'accept-source-route': False, + 'allow-fast-path': True, + 'arp-timeout': '30s', + 'icmp-rate-limit': 20, + 'icmp-rate-mask': '0x1818', + 'ip-forward': True, + 'max-neighbor-entries': 8192, + 'route-cache': True, + 'rp-filter': False, + 'secure-redirects': True, + 'send-redirects': True, + 'tcp-syncookies': False, + }, +] + +START_IP_SETTINGS_OLD_DATA = massage_expected_result_data(START_IP_SETTINGS, ('ip', 'settings')) + +START_IP_ADDRESS = [ + { + '.id': '*1', + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'disabled': False, + }, + { + '.id': '*3', + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': False, + }, + { + '.id': '*F', + 'address': '10.0.0.0/16', + 'interface': 'WAN', + 'disabled': True, + }, +] + +START_IP_ADDRESS_OLD_DATA = massage_expected_result_data(START_IP_ADDRESS, ('ip', 'address')) + +START_IP_DHCP_CLIENT = [ + { + "!comment": None, + "!script": None, + ".id": "*1", + "add-default-route": True, + "default-route-distance": 1, + "dhcp-options": "hostname,clientid", + "disabled": False, + "interface": "ether1", + "use-peer-dns": True, + "use-peer-ntp": True, + }, + { + "!comment": None, + "!dhcp-options": None, + "!script": None, + ".id": "*2", + "add-default-route": True, + "default-route-distance": 1, + "disabled": False, + "interface": "ether2", + "use-peer-dns": True, + "use-peer-ntp": True, + }, + { + "!comment": None, + "!script": None, + ".id": "*3", + "add-default-route": True, + "default-route-distance": 1, + "dhcp-options": "hostname", + "disabled": False, + "interface": "ether3", + "use-peer-dns": True, + "use-peer-ntp": True, + }, +] + +START_IP_DHCP_CLIENT_OLD_DATA = massage_expected_result_data(START_IP_DHCP_CLIENT, ('ip', 'dhcp-client')) + +START_IP_DHCP_SERVER_LEASE = [ + { + '.id': '*1', + 'address': '192.168.88.2', + 'mac-address': '11:22:33:44:55:66', + 'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2', + 'address-lists': '', + 'server': 'main', + 'dhcp-option': '', + 'status': 'waiting', + 'last-seen': 'never', + 'radius': False, + 'dynamic': False, + 'blocked': False, + 'disabled': False, + 'comment': 'foo', + }, + { + '.id': '*2', + 'address': '192.168.88.3', + 'mac-address': '11:22:33:44:55:77', + 'client-id': '1:2:3:4:5:6:7', + 'address-lists': '', + 'server': 'main', + 'dhcp-option': '', + 'status': 'bound', + 'expires-after': '3d7m8s', + 'last-seen': '1m52s', + 'active-address': '192.168.88.14', + 'active-mac-address': '11:22:33:44:55:76', + 'active-client-id': '1:2:3:4:5:6:7', + 'active-server': 'main', + 'host-name': 'bar', + 'radius': False, + 'dynamic': False, + 'blocked': False, + 'disabled': False, + }, + { + '.id': '*3', + 'address': '0.0.0.1', + 'mac-address': '00:00:00:00:00:01', + 'address-lists': '', + 'dhcp-option': '', + 'status': 'waiting', + 'last-seen': 'never', + 'radius': False, + 'dynamic': False, + 'blocked': False, + 'disabled': False, + }, + { + '.id': '*4', + 'address': '0.0.0.2', + 'mac-address': '00:00:00:00:00:02', + 'address-lists': '', + 'dhcp-option': '', + 'status': 'waiting', + 'last-seen': 'never', + 'radius': False, + 'dynamic': False, + 'blocked': False, + 'disabled': False, + }, +] + +START_IP_DHCP_SERVER_LEASE_OLD_DATA = massage_expected_result_data(START_IP_DHCP_SERVER_LEASE, ('ip', 'dhcp-server', 'lease')) + +START_INTERFACE_LIST = [ + { + '.id': '*2000000', + 'name': 'all', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': True, + 'comment': 'contains all interfaces', + }, + { + '.id': '*2000001', + 'name': 'none', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': True, + 'comment': 'contains no interfaces', + }, + { + '.id': '*2000010', + 'name': 'WAN', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': False, + 'comment': 'defconf', + }, + { + '.id': '*2000011', + 'name': 'Foo', + 'dynamic': False, + 'include': '', + 'exclude': '', + 'builtin': False, + 'comment': '', + }, +] + +START_INTERFACE_LIST_OLD_DATA = massage_expected_result_data(START_INTERFACE_LIST, ('interface', 'list'), remove_builtin=True) + +START_INTERFACE_GRE = [ + { + '.id': '*10', + 'name': 'gre-tunnel3', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '0.0.0.0', + 'remote-address': '192.168.1.1', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + }, + { + '.id': '*11', + 'name': 'gre-tunnel4', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '0.0.0.0', + 'remote-address': '192.168.1.2', + 'keepalive': '10s,10', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + }, + { + '.id': '*12', + 'name': 'gre-tunnel5', + 'mtu': 'auto', + 'actual-mtu': 65496, + 'local-address': '192.168.0.1', + 'remote-address': '192.168.1.3', + 'keepalive': '20s,20', + 'dscp': 'inherit', + 'clamp-tcp-mss': True, + 'dont-fragment': False, + 'allow-fast-path': True, + 'running': True, + 'disabled': False, + 'comment': 'foo', + }, +] + +START_INTERFACE_GRE_OLD_DATA = massage_expected_result_data(START_INTERFACE_GRE, ('interface', 'gre')) + + +class TestRouterosApiModifyModule(ModuleTestCase): + + def setUp(self): + super(TestRouterosApiModifyModule, self).setUp() + self.module = api_modify + self.module.LibRouterosError = FakeLibRouterosError + self.module.connect = MagicMock(new=fake_ros_api) + self.module.check_has_library = MagicMock() + self.patch_create_api = patch( + 'ansible_collections.community.routeros.plugins.modules.api_modify.create_api', + MagicMock(new=fake_ros_api)) + self.patch_create_api.start() + self.patch_get_api_version = patch( + 'ansible_collections.community.routeros.plugins.modules.api_modify.get_api_version', + MagicMock(return_value=FAKE_ROS_VERSION)) + self.patch_get_api_version.start() + self.config_module_args = { + 'username': 'admin', + 'password': 'pаss', + 'hostname': '127.0.0.1', + } + + def tearDown(self): + self.patch_get_api_version.stop() + self.patch_create_api.stop() + + def test_module_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson) as exc: + with set_module_args({}): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + + def test_invalid_path(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'something invalid', + 'data': [], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'].startswith('value of path must be one of: '), True) + + def test_invalid_option(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'name': 'baz', + 'foo': 'bar', + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Unknown key "foo" at index 1.') + + def test_invalid_disabled_and_enabled_option(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'name': 'baz', + 'comment': 'foo', + '!comment': None, + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Not both "comment" and "!comment" must appear at index 1.') + + def test_invalid_disabled_option(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'name': 'foo', + '!disabled': None, + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Key "!disabled" must not be disabled (leading "!") at index 1.') + + def test_invalid_disabled_option_value(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'name': 'baz', + '!comment': 'foo', + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Disabled key "!comment" must not have a value at index 1.') + + def test_invalid_non_disabled_option_value(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'name': None, + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Key "name" must not be disabled (value null/~/None) at index 1.') + + def test_invalid_required_missing(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dhcp-server', + 'data': [{ + 'interface': 'eth0', + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Every element in data must contain "name". For example, the element at index #1 does not provide it.') + + def test_invalid_required_one_of_missing(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'address': '192.168.88.1', + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Every element in data must contain one of "name", "regexp". For example, the element at index 1 does not provide it.') + + def test_invalid_mutually_exclusive_both(self): + with self.assertRaises(AnsibleFailJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [{ + 'name': 'foo', + 'regexp': 'bar', + 'address': '192.168.88.1', + }], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'], 'Keys "name", "regexp" cannot be used at the same time at index 1.') + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_idempotent(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + '.id': 'bam', # this should be ignored + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + { + 'comment': None, + 'name': 'router', + 'text': 'Router Text Entry', + }, + ], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_idempotent_2(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'foo', + 'comment': '', + 'address': '192.168.88.2', + }, + { + 'name': 'router', + '!comment': None, + 'text': 'Router Text Entry', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_idempotent_3(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + ], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DNS_STATIC_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_sync_list_add(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'text': 'Router Text Entry', + }, + { + 'name': 'router', + 'text': 'Router Text Entry 2', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + ], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*NEW1', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_sync_list_modify_1(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'text': 'Router Text Entry 2', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + ], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*NEW1', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_modify_1_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'text': 'Router Text Entry 2', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + ], + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + 'name': 'router', + 'text': 'Router Text Entry 2', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_sync_list_modify_2(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': '', + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'text': 'Router Text Entry 2', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_modify_2_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': '', + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'text': 'Router Text Entry 2', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_sync_list_modify_3(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + '!comment': None, + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'cname': 'router.com.', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*NEW1', + 'name': 'router', + 'cname': 'router.com.', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_modify_3_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + '!comment': None, + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'cname': 'router.com.', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + 'name': 'router', + 'cname': 'router.com.', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_sync_list_modify_4(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'comment': 'defconf', + 'text': 'Router Text Entry 2', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'comment': 'defconf', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_modify_4_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'name': 'router', + 'address': '192.168.88.1', + }, + { + 'name': 'router', + 'comment': 'defconf', + 'text': 'Router Text Entry 2', + }, + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'comment': 'defconf', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_sync_list_delete(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_delete_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC)) + def test_sync_list_reorder(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + { + 'name': 'foo', + 'text': 'bar', + }, + { + 'name': 'router', + 'text': 'Router Text Entry', + }, + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + 'ensure_order': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*NEW1', + 'name': 'foo', + 'text': 'bar', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dns', 'static'), START_IP_DNS_STATIC, read_only=True)) + def test_sync_list_reorder_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dns static', + 'data': [ + { + 'name': 'foo', + 'address': '192.168.88.2', + }, + { + 'name': 'foo', + 'text': 'bar', + }, + { + 'name': 'router', + 'text': 'Router Text Entry', + }, + { + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + 'ensure_order': True, + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_DNS_STATIC_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + 'name': 'foo', + 'text': 'bar', + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': False, + 'match-subdomain': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS, read_only=True)) + def test_sync_value_idempotent(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip settings', + 'data': [ + { + 'arp-timeout': '30s', + 'icmp-rate-limit': 20, + 'icmp-rate-mask': '0x1818', + 'ip-forward': True, + 'max-neighbor-entries': 8192, + }, + ], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_SETTINGS_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS, read_only=True)) + def test_sync_value_idempotent_2(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip settings', + 'data': [ + { + 'accept-redirects': True, + 'icmp-rate-limit': 20, + }, + ], + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_SETTINGS_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS)) + def test_sync_value_modify(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip settings', + 'data': [ + { + 'accept-redirects': True, + 'accept-source-route': True, + 'max-neighbor-entries': 4096, + }, + ], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + 'accept-redirects': True, + 'accept-source-route': True, + 'allow-fast-path': True, + 'arp-timeout': '30s', + 'icmp-rate-limit': 20, + 'icmp-rate-mask': '0x1818', + 'ip-forward': True, + 'max-neighbor-entries': 4096, + 'route-cache': True, + 'rp-filter': False, + 'secure-redirects': True, + 'send-redirects': True, + 'tcp-syncookies': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS, read_only=True)) + def test_sync_value_modify_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip settings', + 'data': [ + { + 'accept-redirects': True, + 'accept-source-route': True, + 'max-neighbor-entries': 4096, + }, + ], + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + 'accept-redirects': True, + 'accept-source-route': True, + 'allow-fast-path': True, + 'arp-timeout': '30s', + 'icmp-rate-limit': 20, + 'icmp-rate-mask': '0x1818', + 'ip-forward': True, + 'max-neighbor-entries': 4096, + 'route-cache': True, + 'rp-filter': False, + 'secure-redirects': True, + 'send-redirects': True, + 'tcp-syncookies': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS)) + def test_sync_value_modify_2(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip settings', + 'data': [ + { + 'accept-redirects': True, + 'accept-source-route': True, + 'max-neighbor-entries': 4096, + }, + ], + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + 'accept-redirects': True, + 'accept-source-route': True, + 'allow-fast-path': True, + 'arp-timeout': '30s', + 'icmp-rate-limit': 10, + 'icmp-rate-mask': '0x1818', + 'ip-forward': True, + 'max-neighbor-entries': 4096, + 'route-cache': True, + 'rp-filter': False, + 'secure-redirects': True, + 'send-redirects': True, + 'tcp-syncookies': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'settings'), START_IP_SETTINGS, read_only=True)) + def test_sync_value_modify_2_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip settings', + 'data': [ + { + 'accept-redirects': True, + 'accept-source-route': True, + 'max-neighbor-entries': 4096, + }, + ], + 'handle_entries_content': 'remove', + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_SETTINGS_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + 'accept-redirects': True, + 'accept-source-route': True, + 'allow-fast-path': True, + 'arp-timeout': '30s', + 'icmp-rate-limit': 10, + 'icmp-rate-mask': '0x1818', + 'ip-forward': True, + 'max-neighbor-entries': 4096, + 'route-cache': True, + 'rp-filter': False, + 'secure-redirects': True, + 'send-redirects': True, + 'tcp-syncookies': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'address'), START_IP_ADDRESS, read_only=True)) + def test_sync_primary_key_idempotent(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip address', + 'data': [ + { + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'comment': '', + }, + { + 'address': '192.168.88.0/24', + 'interface': 'bridge', + '!comment': None, + }, + ], + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_ADDRESS_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'address'), START_IP_ADDRESS, read_only=True)) + def test_sync_primary_key_idempotent_2(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip address', + 'data': [ + { + 'address': '192.168.88.0/24', + 'interface': 'bridge', + }, + { + 'address': '10.0.0.0/16', + 'interface': 'WAN', + 'disabled': True, + '!comment': '', + }, + { + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': False, + 'comment': None, + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_ADDRESS_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'address'), START_IP_ADDRESS)) + def test_sync_primary_key_cru(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip address', + 'data': [ + { + 'address': '10.10.0.0/16', + 'interface': 'WIFI', + }, + { + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': True, + }, + { + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'comment': 'foo', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'comment': 'foo', + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'disabled': False, + }, + { + '.id': '*3', + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': True, + }, + { + '.id': '*NEW1', + 'address': '10.10.0.0/16', + 'interface': 'WIFI', + 'disabled': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'address'), START_IP_ADDRESS, read_only=True)) + def test_sync_primary_key_cru_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip address', + 'data': [ + { + 'address': '10.10.0.0/16', + 'interface': 'WIFI', + }, + { + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': True, + }, + { + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'comment': 'foo', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*1', + 'comment': 'foo', + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'disabled': False, + }, + { + '.id': '*3', + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': True, + }, + { + 'address': '10.10.0.0/16', + 'interface': 'WIFI', + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'address'), START_IP_ADDRESS)) + def test_sync_primary_key_cru_reorder(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip address', + 'data': [ + { + 'address': '10.10.0.0/16', + 'interface': 'WIFI', + }, + { + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': True, + }, + { + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'comment': 'foo', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + 'ensure_order': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + '.id': '*NEW1', + 'address': '10.10.0.0/16', + 'interface': 'WIFI', + 'disabled': False, + }, + { + '.id': '*3', + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': True, + }, + { + '.id': '*1', + 'comment': 'foo', + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'disabled': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'address'), START_IP_ADDRESS, read_only=True)) + def test_sync_primary_key_cru_reorder_check(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip address', + 'data': [ + { + 'address': '10.10.0.0/16', + 'interface': 'WIFI', + }, + { + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': True, + }, + { + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'comment': 'foo', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + 'ensure_order': True, + '_ansible_check_mode': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], START_IP_ADDRESS_OLD_DATA) + self.assertEqual(result['new_data'], [ + { + 'address': '10.10.0.0/16', + 'interface': 'WIFI', + }, + { + '.id': '*3', + 'address': '192.168.1.0/24', + 'interface': 'LAN', + 'disabled': True, + }, + { + '.id': '*1', + 'comment': 'foo', + 'address': '192.168.88.0/24', + 'interface': 'bridge', + 'disabled': False, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dhcp-server', 'lease'), START_IP_DHCP_SERVER_LEASE, read_only=True)) + def test_absent_value(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dhcp-server lease', + 'data': [ + { + 'address': '192.168.88.2', + 'mac-address': '11:22:33:44:55:66', + 'client-id': 'ff:1:2:3:4:5:6:7:8:9:a:b:c:d:e:f:0:1:2', + 'server': 'main', + 'comment': 'foo', + }, + { + 'address': '192.168.88.3', + 'mac-address': '11:22:33:44:55:77', + 'client-id': '1:2:3:4:5:6:7', + 'server': 'main', + }, + { + 'address': '0.0.0.1', + 'mac-address': '00:00:00:00:00:01', + 'server': 'all', + }, + { + 'address': '0.0.0.2', + 'mac-address': '00:00:00:00:00:02', + 'server': 'all', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + 'ensure_order': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DHCP_SERVER_LEASE_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DHCP_SERVER_LEASE_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dhcp-client'), START_IP_DHCP_CLIENT, read_only=True)) + def test_default_remove_combination_idempotent(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dhcp-client', + 'data': [ + { + 'interface': 'ether1', + }, + { + 'interface': 'ether2', + 'dhcp-options': None, + }, + { + 'interface': 'ether3', + 'dhcp-options': 'hostname', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + 'ensure_order': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_IP_DHCP_CLIENT_OLD_DATA) + self.assertEqual(result['new_data'], START_IP_DHCP_CLIENT_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('ip', 'dhcp-client'), [])) + def test_default_remove_combination_create(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'ip dhcp-client', + 'data': [ + { + 'interface': 'ether1', + }, + { + 'interface': 'ether2', + 'dhcp-options': None, + }, + { + 'interface': 'ether3', + 'dhcp-options': 'hostname', + }, + ], + 'handle_absent_entries': 'remove', + 'handle_entries_content': 'remove', + 'ensure_order': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + self.assertEqual(result['old_data'], []) + self.assertEqual(result['new_data'], [ + { + ".id": "*NEW1", + "add-default-route": True, + "default-route-distance": 1, + "dhcp-options": "hostname,clientid", + "disabled": False, + "interface": "ether1", + "use-peer-dns": True, + "use-peer-ntp": True, + }, + { + # "!dhcp-options": None, + ".id": "*NEW2", + "add-default-route": True, + "default-route-distance": 1, + "disabled": False, + "interface": "ether2", + "use-peer-dns": True, + "use-peer-ntp": True, + }, + { + ".id": "*NEW3", + "add-default-route": True, + "default-route-distance": 1, + "dhcp-options": "hostname", + "disabled": False, + "interface": "ether3", + "use-peer-dns": True, + "use-peer-ntp": True, + }, + ]) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('interface', 'list'), START_INTERFACE_LIST, read_only=True)) + def test_absent_entries_builtin(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'interface list', + 'data': [ + { + 'name': 'WAN', + 'comment': 'defconf', + }, + { + 'name': 'Foo', + }, + ], + 'handle_absent_entries': 'remove', + 'ensure_order': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_INTERFACE_LIST_OLD_DATA) + self.assertEqual(result['new_data'], START_INTERFACE_LIST_OLD_DATA) + + @patch('ansible_collections.community.routeros.plugins.modules.api_modify.compose_api_path', + new=create_fake_path(('interface', 'gre'), START_INTERFACE_GRE, read_only=True)) + def test_idempotent_default_disabled(self): + with self.assertRaises(AnsibleExitJson) as exc: + args = self.config_module_args.copy() + args.update({ + 'path': 'interface gre', + 'data': [ + { + 'name': 'gre-tunnel3', + 'remote-address': '192.168.1.1', + '!keepalive': None, + }, + { + 'name': 'gre-tunnel4', + 'remote-address': '192.168.1.2', + }, + { + 'name': 'gre-tunnel5', + 'local-address': '192.168.0.1', + 'remote-address': '192.168.1.3', + 'keepalive': '20s,20', + 'comment': 'foo', + }, + ], + 'handle_absent_entries': 'remove', + 'ensure_order': True, + }) + with set_module_args(args): + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + self.assertEqual(result['old_data'], START_INTERFACE_GRE_OLD_DATA) + self.assertEqual(result['new_data'], START_INTERFACE_GRE_OLD_DATA) diff --git a/tests/unit/plugins/modules/test_command.py b/tests/unit/plugins/modules/test_command.py index ab045fe..06153f0 100644 --- a/tests/unit/plugins/modules/test_command.py +++ b/tests/unit/plugins/modules/test_command.py @@ -1,19 +1,6 @@ -# (c) 2016 Red Hat Inc. -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . +# Copyright (c) 2016 Red Hat Inc. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) @@ -21,9 +8,10 @@ __metaclass__ = type import json -from ansible_collections.community.routeros.tests.unit.compat.mock import patch +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args + from ansible_collections.community.routeros.plugins.modules import command -from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args from .routeros_module import TestRouterosModule, load_fixture @@ -60,54 +48,54 @@ class TestRouterosCommandModule(TestRouterosModule): self.run_commands.side_effect = load_from_file def test_command_simple(self): - set_module_args(dict(commands=['/system resource print'])) - result = self.execute_module() + with set_module_args(dict(commands=['/system resource print'])): + result = self.execute_module(changed=True) self.assertEqual(len(result['stdout']), 1) self.assertTrue('platform: "MikroTik"' in result['stdout'][0]) def test_command_multiple(self): - set_module_args(dict(commands=['/system resource print', '/system resource print'])) - result = self.execute_module() + with set_module_args(dict(commands=['/system resource print', '/system resource print'])): + result = self.execute_module(changed=True) self.assertEqual(len(result['stdout']), 2) self.assertTrue('platform: "MikroTik"' in result['stdout'][0]) def test_command_wait_for(self): wait_for = 'result[0] contains "MikroTik"' - set_module_args(dict(commands=['/system resource print'], wait_for=wait_for)) - self.execute_module() + with set_module_args(dict(commands=['/system resource print'], wait_for=wait_for)): + self.execute_module(changed=True) def test_command_wait_for_fails(self): wait_for = 'result[0] contains "test string"' - set_module_args(dict(commands=['/system resource print'], wait_for=wait_for)) - self.execute_module(failed=True) + with set_module_args(dict(commands=['/system resource print'], wait_for=wait_for)): + self.execute_module(failed=True) self.assertEqual(self.run_commands.call_count, 10) def test_command_retries(self): wait_for = 'result[0] contains "test string"' - set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, retries=2)) - self.execute_module(failed=True) + with set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, retries=2)): + self.execute_module(failed=True) self.assertEqual(self.run_commands.call_count, 2) def test_command_match_any(self): wait_for = ['result[0] contains "MikroTik"', 'result[0] contains "test string"'] - set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, match='any')) - self.execute_module() + with set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, match='any')): + self.execute_module(changed=True) def test_command_match_all(self): wait_for = ['result[0] contains "MikroTik"', 'result[0] contains "RB1100"'] - set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, match='all')) - self.execute_module() + with set_module_args(dict(commands=['/system resource print'], wait_for=wait_for, match='all')): + self.execute_module(changed=True) def test_command_match_all_failure(self): wait_for = ['result[0] contains "MikroTik"', 'result[0] contains "test string"'] commands = ['/system resource print', '/system resource print'] - set_module_args(dict(commands=commands, wait_for=wait_for, match='all')) - self.execute_module(failed=True) + with set_module_args(dict(commands=commands, wait_for=wait_for, match='all')): + self.execute_module(failed=True) def test_command_wait_for_2(self): wait_for = 'result[0] contains "wireless"' - set_module_args(dict(commands=['/system package print'], wait_for=wait_for)) - self.execute_module() + with set_module_args(dict(commands=['/system package print'], wait_for=wait_for)): + self.execute_module(changed=True) diff --git a/tests/unit/plugins/modules/test_facts.py b/tests/unit/plugins/modules/test_facts.py index ea111df..918f378 100644 --- a/tests/unit/plugins/modules/test_facts.py +++ b/tests/unit/plugins/modules/test_facts.py @@ -1,25 +1,15 @@ -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible_collections.community.routeros.tests.unit.compat.mock import patch +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import set_module_args + from ansible_collections.community.routeros.plugins.modules import facts -from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args from .routeros_module import TestRouterosModule, load_fixture @@ -43,15 +33,15 @@ class TestRouterosFactsModule(TestRouterosModule): output = list() for command in commands: - filename = str(command).split(' | ')[0].replace(' ', '_') + filename = str(command).split(' | ', 1)[0].replace(' ', '_') output.append(load_fixture('facts%s' % filename)) return output self.run_commands.side_effect = load_from_file def test_facts_default(self): - set_module_args(dict(gather_subset='default')) - result = self.execute_module() + with set_module_args(dict(gather_subset='default')): + result = self.execute_module() self.assertEqual( result['ansible_facts']['ansible_net_hostname'], 'MikroTik' ) @@ -72,8 +62,8 @@ class TestRouterosFactsModule(TestRouterosModule): ) def test_facts_hardware(self): - set_module_args(dict(gather_subset='hardware')) - result = self.execute_module() + with set_module_args(dict(gather_subset='hardware')): + result = self.execute_module() self.assertEqual( result['ansible_facts']['ansible_net_spacefree_mb'], 64921.6 ) @@ -88,8 +78,8 @@ class TestRouterosFactsModule(TestRouterosModule): ) def test_facts_config(self): - set_module_args(dict(gather_subset='config')) - result = self.execute_module() + with set_module_args(dict(gather_subset='config')): + result = self.execute_module() self.assertIsInstance( result['ansible_facts']['ansible_net_config'], str ) @@ -99,8 +89,8 @@ class TestRouterosFactsModule(TestRouterosModule): ) def test_facts_interfaces(self): - set_module_args(dict(gather_subset='interfaces')) - result = self.execute_module() + with set_module_args(dict(gather_subset='interfaces')): + result = self.execute_module() self.assertIn( result['ansible_facts']['ansible_net_all_ipv4_addresses'][0], ['10.37.129.3', '10.37.0.0', '192.168.88.1'] ) @@ -129,8 +119,8 @@ class TestRouterosFactsModule(TestRouterosModule): self.assertEqual(result, None) def test_facts_routing(self): - set_module_args(dict(gather_subset='routing')) - result = self.execute_module() + with set_module_args(dict(gather_subset='routing')): + result = self.execute_module() self.assertIn( result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['name'], ['iBGP_BRAS.TYRMA'] ) diff --git a/tests/unit/plugins/modules/utils.py b/tests/unit/plugins/modules/utils.py deleted file mode 100644 index 0468b68..0000000 --- a/tests/unit/plugins/modules/utils.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -import json - -from ansible_collections.community.routeros.tests.unit.compat import unittest -from ansible_collections.community.routeros.tests.unit.compat.mock import patch -from ansible.module_utils import basic -from ansible.module_utils.common.text.converters import to_bytes - - -def set_module_args(args): - if '_ansible_remote_tmp' not in args: - args['_ansible_remote_tmp'] = '/tmp' - if '_ansible_keep_remote_files' not in args: - args['_ansible_keep_remote_files'] = False - - args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) - basic._ANSIBLE_ARGS = to_bytes(args) - - -class AnsibleExitJson(Exception): - pass - - -class AnsibleFailJson(Exception): - pass - - -def exit_json(*args, **kwargs): - if 'changed' not in kwargs: - kwargs['changed'] = False - raise AnsibleExitJson(kwargs) - - -def fail_json(*args, **kwargs): - kwargs['failed'] = True - raise AnsibleFailJson(kwargs) - - -class ModuleTestCase(unittest.TestCase): - - def setUp(self): - self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) - self.mock_module.start() - self.mock_sleep = patch('time.sleep') - self.mock_sleep.start() - set_module_args({}) - self.addCleanup(self.mock_module.stop) - self.addCleanup(self.mock_sleep.stop) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index eac8a96..479f2fc 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -1,4 +1,6 @@ -unittest2 ; python_version <= '2.6' +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later -# requirements for api module -librouteros ; python_version >= '3.6' +unittest2 ; python_version <= '2.6' +ordereddict ; python_version <= '2.6' diff --git a/tests/unit/requirements.yml b/tests/unit/requirements.yml new file mode 100644 index 0000000..107fe12 --- /dev/null +++ b/tests/unit/requirements.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +collections: + - community.internal_test_tools diff --git a/tests/update-docs.py b/tests/update-docs.py new file mode 100644 index 0000000..5f28f8e --- /dev/null +++ b/tests/update-docs.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein (@felixfontein) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +''' +Updates DOCUMENTATION of modules using module_utils._api_data with the correct list of supported paths. +''' + +import sys + +from ansible_collections.community.routeros.plugins.module_utils._api_data import ( + PATHS, + join_path, +) + + +MODULES = [ + 'plugins/modules/api_info.py', + 'plugins/modules/api_modify.py', +] + + +def update_file(file: str, begin_line: str, end_line: str, choice_line: str, path_choices: list[str]) -> bool: + with open(file, 'r', encoding='utf-8') as f: + lines = f.read().splitlines() + begin_index = lines.index(begin_line) + end_index = lines.index(end_line, begin_index + 1) + new_lines = lines[:begin_index + 1] + [choice_line.format(choice=choice) for choice in path_choices] + lines[end_index:] + if lines == new_lines: + return False + print(f'{file} has been updated') + with open(file, 'w', encoding='utf-8') as f: + f.write('\n'.join(new_lines) + '\n') + return True + + +def main(args: list[str]) -> int: + path_choices = sorted([join_path(path) for path, path_info in PATHS.items() if path_info.fully_understood]) + + changes = False + for file in MODULES: + changes |= update_file(file, ' # BEGIN PATH LIST', ' # END PATH LIST', ' - {choice}', path_choices) + + lint = "--lint" in args + if not lint or not changes: + return 0 + + print("Run 'nox -Re update-docs'!") + return 1 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:]))