diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/routeros | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
147 files changed, 19634 insertions, 0 deletions
diff --git a/ansible_collections/community/routeros/.github/dependabot.yml b/ansible_collections/community/routeros/.github/dependabot.yml new file mode 100644 index 000000000..2f4ff900d --- /dev/null +++ b/ansible_collections/community/routeros/.github/dependabot.yml @@ -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: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/ansible_collections/community/routeros/.github/patchback.yml b/ansible_collections/community/routeros/.github/patchback.yml new file mode 100644 index 000000000..5ee7812ed --- /dev/null +++ b/ansible_collections/community/routeros/.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/ansible_collections/community/routeros/.github/workflows/ansible-test.yml b/ansible_collections/community/routeros/.github/workflows/ansible-test.yml new file mode 100644 index 000000000..a5b351913 --- /dev/null +++ b/ansible_collections/community/routeros/.github/workflows/ansible-test.yml @@ -0,0 +1,158 @@ +--- +# 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 + +# For the comprehensive list of the inputs supported by the ansible-community/ansible-test-gh-action GitHub Action, see +# https://github.com/marketplace/actions/ansible-test + +name: CI +on: + # Run CI against all pushes (direct commits, also merged PRs), Pull Requests + push: + branches: + - main + - stable-* + pull_request: + # Run CI once per day (at 05:15 UTC) + schedule: + - cron: '15 5 * * *' + +jobs: + 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 + - stable-2.12 + - stable-2.13 + - stable-2.14 + - stable-2.15 + - devel + # Ansible-test on various stable branches does not yet work well with cgroups v2. + # Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04 + # image for these stable branches. The list of branches where this is necessary will + # shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28 + # for the latest list. + runs-on: >- + ${{ contains(fromJson( + '["stable-2.9", "stable-2.10", "stable-2.11"]' + ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} + steps: + - name: Perform sanity testing + uses: felixfontein/ansible-test-gh-action@main + with: + ansible-core-version: ${{ matrix.ansible }} + testing-type: sanity + # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) + pre-test-cmd: |- + git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.netcommon.git ../../ansible/netcommon + git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.utils.git ../../ansible/utils + + units: + # Ansible-test on various stable branches does not yet work well with cgroups v2. + # Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04 + # image for these stable branches. The list of branches where this is necessary will + # shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28 + # for the latest list. + runs-on: >- + ${{ contains(fromJson( + '["stable-2.9", "stable-2.10", "stable-2.11"]' + ), matrix.ansible) && 'ubuntu-20.04' || '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 + - stable-2.12 + - stable-2.13 + - stable-2.14 + - stable-2.15 + - devel + + steps: + - name: >- + Perform unit testing against + Ansible version ${{ matrix.ansible }} + uses: felixfontein/ansible-test-gh-action@main + with: + ansible-core-version: ${{ matrix.ansible }} + testing-type: units + # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) + pre-test-cmd: |- + git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.netcommon.git ../../ansible/netcommon + git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.utils.git ../../ansible/utils + + integration: + # Ansible-test on various stable branches does not yet work well with cgroups v2. + # Since ubuntu-latest now uses Ubuntu 22.04, we need to fall back to the ubuntu-20.04 + # image for these stable branches. The list of branches where this is necessary will + # shrink over time, check out https://github.com/ansible-collections/news-for-maintainers/issues/28 + # for the latest list. + runs-on: >- + ${{ contains(fromJson( + '["stable-2.9", "stable-2.10", "stable-2.11"]' + ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} + name: I (Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}) + strategy: + fail-fast: false + matrix: + ansible: + - devel + python: + - 3.9 + - "3.10" + - "3.11" + include: + # 2.9 + - ansible: stable-2.9 + python: 2.7 + - ansible: stable-2.9 + python: 3.5 + # 2.10 + - ansible: stable-2.10 + python: 3.5 + # 2.11 + - ansible: stable-2.11 + python: 2.7 + - ansible: stable-2.11 + python: 3.6 + # 2.12 + - ansible: stable-2.12 + python: 3.8 + # 2.13 + - ansible: stable-2.13 + python: "3.10" + # 2.14 + - ansible: stable-2.14 + python: "3.9" + # 2.15 + - ansible: stable-2.15 + python: "3.11" + + steps: + - name: >- + Perform integration testing against + Ansible version ${{ matrix.ansible }} + under Python ${{ matrix.python }} + uses: felixfontein/ansible-test-gh-action@main + with: + ansible-core-version: ${{ matrix.ansible }} + integration-continue-on-error: 'false' + integration-diff: 'false' + integration-retry-on-error: 'true' + # NOTE: we're installing with git to work around Galaxy being a huge PITA (https://github.com/ansible/galaxy/issues/2429) + pre-test-cmd: |- + git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.netcommon.git ../../ansible/netcommon + git clone --depth=1 --single-branch https://github.com/ansible-collections/ansible.utils.git ../../ansible/utils + target-python-version: ${{ matrix.python }} + testing-type: integration diff --git a/ansible_collections/community/routeros/.github/workflows/docs-pr.yml b/ansible_collections/community/routeros/.github/workflows/docs-pr.yml new file mode 100644 index 000000000..4b3f1f373 --- /dev/null +++ b/ansible_collections/community/routeros/.github/workflows/docs-pr.yml @@ -0,0 +1,92 @@ +--- +# 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: '> * `$<status>` [$<path_tail>](https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/pr/${{ github.event.number }}/$<path_tail>)' + + publish-docs-gh-pages: + # for now we won't run this on forks + if: github.repository == 'ansible-collections/community.routeros' + permissions: + contents: 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' }} + 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/ansible_collections/community/routeros/.github/workflows/docs-push.yml b/ansible_collections/community/routeros/.github/workflows/docs-push.yml new file mode 100644 index 000000000..7408bbb6f --- /dev/null +++ b/ansible_collections/community/routeros/.github/workflows/docs-push.yml @@ -0,0 +1,52 @@ +--- +# 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: 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/ + + publish-docs-gh-pages: + # for now we won't run this on forks + if: github.repository == 'ansible-collections/community.routeros' + permissions: + contents: 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 }} + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/ansible_collections/community/routeros/.github/workflows/ee.yml b/ansible_collections/community/routeros/.github/workflows/ee.yml new file mode 100644 index 000000000..406703a07 --- /dev/null +++ b/ansible_collections/community/routeros/.github/workflows/ee.yml @@ -0,0 +1,175 @@ +--- +# 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: execution environment +on: + # Run CI against all pushes (direct commits, also merged PRs), Pull Requests + push: + branches: + - main + - stable-* + pull_request: + # Run CI once per day (at 05:15 UTC) + # This ensures that even if there haven't been commits that we are still testing against latest version of ansible-builder + schedule: + - cron: '15 5 * * *' + +env: + NAMESPACE: community + COLLECTION_NAME: routeros + +jobs: + build: + name: Build and test EE (${{ matrix.name }}) + strategy: + fail-fast: false + matrix: + name: + - '' + ansible_core: + - '' + ansible_runner: + - '' + base_image: + - '' + pre_base: + - '' + extra_vars: + - '' + other_deps: + - '' + exclude: + - ansible_core: '' + include: + - name: ansible-core devel @ RHEL UBI 9 + ansible_core: https://github.com/ansible/ansible/archive/devel.tar.gz + ansible_runner: ansible-runner + base_image: docker.io/redhat/ubi9:latest + pre_base: '"#"' + - name: ansible-core 2.15 @ Rocky Linux 9 + ansible_core: https://github.com/ansible/ansible/archive/stable-2.15.tar.gz + ansible_runner: ansible-runner + base_image: quay.io/rockylinux/rockylinux:9 + pre_base: '"#"' + - name: ansible-core 2.14 @ CentOS Stream 9 + ansible_core: https://github.com/ansible/ansible/archive/stable-2.14.tar.gz + ansible_runner: ansible-runner + base_image: quay.io/centos/centos:stream9 + pre_base: '"#"' + - name: ansible-core 2.13 @ RHEL UBI 8 + ansible_core: https://github.com/ansible/ansible/archive/stable-2.13.tar.gz + ansible_runner: ansible-runner + other_deps: |2 + python_interpreter: + package_system: python39 python39-pip python39-wheel python39-cryptography + base_image: docker.io/redhat/ubi8:latest + pre_base: '"#"' + - name: ansible-core 2.12 @ CentOS Stream 8 + ansible_core: https://github.com/ansible/ansible/archive/stable-2.12.tar.gz + ansible_runner: ansible-runner + other_deps: |2 + python_interpreter: + package_system: python39 python39-pip python39-wheel python39-cryptography + base_image: quay.io/centos/centos:stream8 + pre_base: '"#"' + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + with: + path: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install ansible-builder and ansible-navigator + run: pip install ansible-builder ansible-navigator + + - name: Verify requirements + run: ansible-builder introspect --sanitize . + + - 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: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }} + + - name: Build collection + run: | + ansible-galaxy collection build --output-path ../../../ + working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }} + + - name: Create files for building execution environment + run: | + COLLECTION_FILENAME="$(ls "${{ env.NAMESPACE }}-${{ env.COLLECTION_NAME }}"-*.tar.gz)" + + # EE config + cat > execution-environment.yml <<EOF + --- + version: 3 + dependencies: + ansible_core: + package_pip: ${{ matrix.ansible_core }} + ansible_runner: + package_pip: ${{ matrix.ansible_runner }} + galaxy: requirements.yml + ${{ matrix.other_deps }} + + images: + base_image: + name: ${{ matrix.base_image }} + + additional_build_files: + - src: ${COLLECTION_FILENAME} + dest: src + + additional_build_steps: + prepend_base: + - ${{ matrix.pre_base }} + EOF + echo "::group::execution-environment.yml" + cat execution-environment.yml + echo "::endgroup::" + + # Requirements + cat > requirements.yml <<EOF + --- + collections: + - name: src/${COLLECTION_FILENAME} + type: file + EOF + echo "::group::requirements.yml" + cat requirements.yml + echo "::endgroup::" + + - name: Build image based on ${{ matrix.base_image }} + run: | + ansible-builder build --verbosity 3 --tag test-ee:latest --container-runtime docker + + - name: Show images + run: docker image ls + + - name: Run basic tests + run: > + ansible-navigator run + --mode stdout + --container-engine docker + --pull-policy never + --set-environment-variable ANSIBLE_PRIVATE_ROLE_VARS=true + --execution-environment-image test-ee:latest + -v + all.yml + ${{ matrix.extra_vars }} + working-directory: ansible_collections/${{ env.NAMESPACE }}/${{ env.COLLECTION_NAME }}/tests/ee diff --git a/ansible_collections/community/routeros/.github/workflows/extra-tests.yml b/ansible_collections/community/routeros/.github/workflows/extra-tests.yml new file mode 100644 index 000000000..0bbcdbb38 --- /dev/null +++ b/ansible_collections/community/routeros/.github/workflows/extra-tests.yml @@ -0,0 +1,48 @@ +--- +# 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: extra-tests +on: + # Run CI against all pushes (direct commits, also merged PRs), Pull Requests + push: + branches: + - main + - stable-* + pull_request: + # Run CI once per day (at 05:15 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: '15 5 * * *' +env: + NAMESPACE: community + COLLECTION_NAME: routeros + +jobs: + extra-sanity: + name: Extra Sanity + runs-on: ubuntu-latest + steps: + + - name: Check out code + uses: actions/checkout@v3 + with: + path: ansible_collections/${{env.NAMESPACE}}/${{env.COLLECTION_NAME}} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - 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/ansible_collections/community/routeros/.github/workflows/import-galaxy.yml b/ansible_collections/community/routeros/.github/workflows/import-galaxy.yml new file mode 100644 index 000000000..55a731035 --- /dev/null +++ b/ansible_collections/community/routeros/.github/workflows/import-galaxy.yml @@ -0,0 +1,88 @@ +--- +# 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: import-galaxy +on: + # Run CI against all pushes (direct commits, also merged PRs) to main, and all Pull Requests + push: + branches: + - main + - stable-* + 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@v3 + with: + path: ./checkout + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - 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@v3 + 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@v4 + with: + python-version: '3.10' + + - 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@v3 + 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/ansible_collections/community/routeros/.github/workflows/reuse.yml b/ansible_collections/community/routeros/.github/workflows/reuse.yml new file mode 100644 index 000000000..acd7bc8a7 --- /dev/null +++ b/ansible_collections/community/routeros/.github/workflows/reuse.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 + +name: Verify REUSE + +on: + push: + branches: [main] + pull_request: + branches: [main] + # Run CI once per day (at 05:15 UTC) + schedule: + - cron: '15 5 * * *' + +jobs: + check: + permissions: + contents: read + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: | + pip install reuse + + - name: Check REUSE compliance + run: | + reuse lint diff --git a/ansible_collections/community/routeros/.reuse/dep5 b/ansible_collections/community/routeros/.reuse/dep5 new file mode 100644 index 000000000..0c3745ebf --- /dev/null +++ b/ansible_collections/community/routeros/.reuse/dep5 @@ -0,0 +1,5 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: changelogs/fragments/* +Copyright: Ansible Project +License: GPL-3.0-or-later diff --git a/ansible_collections/community/routeros/CHANGELOG.rst b/ansible_collections/community/routeros/CHANGELOG.rst new file mode 100644 index 000000000..d82c58e47 --- /dev/null +++ b/ansible_collections/community/routeros/CHANGELOG.rst @@ -0,0 +1,388 @@ +================================ +Community RouterOS Release Notes +================================ + +.. contents:: Topics + + +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 implemeted for ROS6 due to the significat 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 <https://reuse.software/spec/>`__ 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 +----------- + +- api_info - Retrieve information from API +- 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 +----------- + +- api_facts - Collect facts from remote devices running MikroTik RouterOS using the API +- 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 +~~~~~~ + +- join - Join a list of arguments to a command +- list_to_dict - Convert a list of arguments to a list of dictionary +- quote_argument - Quote an argument +- quote_argument_value - Quote an argument value +- 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). +- 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). + +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). + +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). +- facts - added support for a dash (``-``) in username (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). + +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). + +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). + +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). +- facts - set configuration export on to verbose, for full configuration export (https://github.com/ansible-collections/community.network/pull/104). diff --git a/ansible_collections/community/routeros/CHANGELOG.rst.license b/ansible_collections/community/routeros/CHANGELOG.rst.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/COPYING b/ansible_collections/community/routeros/COPYING new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/ansible_collections/community/routeros/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/ansible_collections/community/routeros/FILES.json b/ansible_collections/community/routeros/FILES.json new file mode 100644 index 000000000..f0a44a030 --- /dev/null +++ b/ansible_collections/community/routeros/FILES.json @@ -0,0 +1,1286 @@ +{ + "files": [ + { + "name": ".", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows/ansible-test.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "21b4409514745adc855018f435409f1ec11567dc50c0c9656df0fe8ac8070b66", + "format": 1 + }, + { + "name": ".github/workflows/docs-pr.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7447bfd97439ddb95181d05a354af0baaa6075b67897997d2f19e0f140394120", + "format": 1 + }, + { + "name": ".github/workflows/docs-push.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "90515807b7cc45d54a09d6392547c15f802774a4a4f9d3a2953f7f846ac0e12d", + "format": 1 + }, + { + "name": ".github/workflows/ee.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cd5a0b5b9f6f084d5fdc49f64fd4fbdc279102cf317be836dcbee3265c8a9cfb", + "format": 1 + }, + { + "name": ".github/workflows/extra-tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4124755ad295f3e9b6cf2968192fb8d8fd54f8faa0df601b80c0aec4d68b45f0", + "format": 1 + }, + { + "name": ".github/workflows/import-galaxy.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "310dc7ba44c93f61f5f88b323259f6efb8925681bfbbb15a1a18d774af54dcd6", + "format": 1 + }, + { + "name": ".github/workflows/reuse.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "05ee22080e7de7265b2839359ee290b4e81162e089d27d01d59d4af12d54761f", + "format": 1 + }, + { + "name": ".github/dependabot.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f314f2fce46d346ef9559fdeae3a55ce6d799bab45a255092446c31e17e6874d", + "format": 1 + }, + { + "name": ".github/patchback.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6a23e48e2562604318540e6ddcac75213ad2c367258d76fc75914e9b939d380e", + "format": 1 + }, + { + "name": ".reuse", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".reuse/dep5", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9d8cb20d72d7e81aaf2e7f0ddce7eacdb3d47a890541717d7fae08a6cab7ebed", + "format": 1 + }, + { + "name": "LICENSES", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "LICENSES/GPL-3.0-or-later.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986", + "format": 1 + }, + { + "name": "LICENSES/BSD-2-Clause.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f11e51ed1eec39ad21d458ba44d805807a301c17ee9fe39538ccc9e2b280936c", + "format": 1 + }, + { + "name": "LICENSES/PSF-2.0.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83b042fc7d6aca0f10d68e45efa56b9bc0a1496608e7e7728fe09d1a534a054a", + "format": 1 + }, + { + "name": "changelogs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "changelogs/fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "changelogs/fragments/.keep", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "changelogs/changelog.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6269eee40124bb0bb68d94fa7e8c8e42c24988985220832aa0c43e056c60af85", + "format": 1 + }, + { + "name": "changelogs/changelog.yaml.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "changelogs/config.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "73b52630d73b33f7f7bf31cd751b732a60f8e3db798afe416d6c6ba67ca80044", + "format": 1 + }, + { + "name": "docs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite/rst", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/docsite/rst/api-guide.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "03b35df33b16d917e7fb4d1b975ebfd72e1b050f8a1b25591856fc0d149758f3", + "format": 1 + }, + { + "name": "docs/docsite/rst/quoting.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "635d3a7c79d2a2aa2d49861ea06979b30261a994875aec510fded7e08c0d394f", + "format": 1 + }, + { + "name": "docs/docsite/rst/ssh-guide.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "42fe9bd067713f5a3ffa9f12d38f773c554524bcb36723763382e997baff5d50", + "format": 1 + }, + { + "name": "docs/docsite/extra-docs.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "56b4c3afedff91c8ebe088bba84fa5d9d1c55b0a8dfa0bfab9695a5f3a2096e2", + "format": 1 + }, + { + "name": "docs/docsite/links.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "af08dfe0bc69484489533cdb33ce0360d096e9b6658b23c0bedd35f4f42c7eb2", + "format": 1 + }, + { + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/ee-requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "46cb840dfb4f8b7cd349a462277d619d7b4f71f3362fb9d6af0ae6d548ff01cd", + "format": 1 + }, + { + "name": "meta/execution-environment.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "acef6a0f697d7eb98f771428fc62ec80223e69febd5ba82ab56e2e85a600b398", + "format": 1 + }, + { + "name": "meta/runtime.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8b89ebdd863cdc698e411dea4449ef9cde02ac8b62728e35f960d5f42763adc2", + "format": 1 + }, + { + "name": "plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/cliconf", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/cliconf/routeros.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "493500e79c9fab7acb576a4437cf62aff26fc6b223eaa5806fd248473141ca16", + "format": 1 + }, + { + "name": "plugins/doc_fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/doc_fragments/api.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6e1855a22deb8f5a21cadb3594afd67b93e9d0a1500f652d5f558b622948d2b6", + "format": 1 + }, + { + "name": "plugins/doc_fragments/attributes.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2267056c335acfc6bdeea2e0b3f8c0ddea8d69b4780795a3de5ed33187cc4bab", + "format": 1 + }, + { + "name": "plugins/filter", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/filter/join.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "71cafc4b104c0e2050688811d5ab8794adba042ccc32da9d88bf66db9b3987d0", + "format": 1 + }, + { + "name": "plugins/filter/list_to_dict.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "21837e13bd533aeba2e1843375636b607db7f619d856fd3d2c3e1a69cc953a41", + "format": 1 + }, + { + "name": "plugins/filter/quote_argument.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6c16f5da12889a009abc9b91c3379dff3048660823831f2092c879418268e82b", + "format": 1 + }, + { + "name": "plugins/filter/quote_argument_value.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a677f85c5c264b356fa4d14af396e2f495a44543814483b3b0a6f4b0cb4022de", + "format": 1 + }, + { + "name": "plugins/filter/quoting.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "084f95d3fa77bb6dc800b28e8456ccf3254e64e6768d3a874d3525b07e4c3f9b", + "format": 1 + }, + { + "name": "plugins/filter/split.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b02e7a8fca0927e2ba0c7545ebeb1da364e4aa2fbf3fd1d20c88b5c6a97f81d0", + "format": 1 + }, + { + "name": "plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/_api_data.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "07633c6f88a503e55a4560be261bbb7867cc8483b5ec2dcfa9984b5cc32410a5", + "format": 1 + }, + { + "name": "plugins/module_utils/_version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "44a3a049aaa06add6402c1091c1f0c57664b3c1a928ec3b0651fe0b930c71e4b", + "format": 1 + }, + { + "name": "plugins/module_utils/api.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f3795f24d8089bd6dc90de58ba96126c3468e9ead8f4813b14f48af48e9c2f63", + "format": 1 + }, + { + "name": "plugins/module_utils/quoting.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ef82865d6cff69ee50e4f264d84b53d6a96eeb50c3f3f9287cd804af8e4010ba", + "format": 1 + }, + { + "name": "plugins/module_utils/routeros.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a676d3c4ed2dd7f928139aff4728a8d66029145fdf88f0d5a6625188e9ca20b5", + "format": 1 + }, + { + "name": "plugins/module_utils/version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a633151e8a76c7f36703fd1c20f65c9b2cb0128fa3aa6c804ef70fe1fab56fb2", + "format": 1 + }, + { + "name": "plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/modules/api.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "751d2740488997e5c6a41d0c4255059b0dfa5755f8e8d4cafb5a7edc5071bc23", + "format": 1 + }, + { + "name": "plugins/modules/api_facts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2b19e626afe7b30a27896a18d65c1ffdb4ae9446c90b52d01874543d84705590", + "format": 1 + }, + { + "name": "plugins/modules/api_find_and_modify.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0488928f752e283ae5fa10efe58c56b887840e29f43fa1fbdf65a99b6a6fc4d1", + "format": 1 + }, + { + "name": "plugins/modules/api_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9c3c6d4248fee5ec8866cf18848613143b6c12f0972f8daca1886b7279d737ae", + "format": 1 + }, + { + "name": "plugins/modules/api_modify.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ff23dd65b926b1ad9d7de9d8d4fb1bf699a6ad0b49b48751fa95fa40b833712", + "format": 1 + }, + { + "name": "plugins/modules/command.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "de6485210c86278b0f8b388f85b71ec26e85d914fa8b11c8c76a0ea8342977fb", + "format": 1 + }, + { + "name": "plugins/modules/facts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "016fffb2831186450dc45cbf680f490963ae9ce26b8a41c4366821eb5146469c", + "format": 1 + }, + { + "name": "plugins/terminal", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/terminal/routeros.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "be7db0199bb88d34a6588f42c15a568ac203e4b64b9e58520be41bb2bea1e701", + "format": 1 + }, + { + "name": "tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/filter_quoting", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/filter_quoting/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/filter_quoting/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4a1c52aebecb55d6aca9d1b260f98b2b4dabc8358004017bcab8fbefb872f662", + "format": 1 + }, + { + "name": "tests/ee/roles/filter_quoting/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7276bae33c365267a01c3eccd6370428d42943b43f397b5412be41729173f617", + "format": 1 + }, + { + "name": "tests/ee/roles/smoke", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/smoke/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/ee/roles/smoke/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7f12077ee91537d97d937d8839121660ab2f348a61ec276a86c4681157ec6927", + "format": 1 + }, + { + "name": "tests/ee/all.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "15124f745ece7c6afe94201049f4b301a64c1ebaef64568ec1c92f429c27e0de", + "format": 1 + }, + { + "name": "tests/integration", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_quoting", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_quoting/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/filter_quoting/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4a1c52aebecb55d6aca9d1b260f98b2b4dabc8358004017bcab8fbefb872f662", + "format": 1 + }, + { + "name": "tests/integration/targets/filter_quoting/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7276bae33c365267a01c3eccd6370428d42943b43f397b5412be41729173f617", + "format": 1 + }, + { + "name": "tests/integration/requirements.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "577b55fc276c5bcd3d0afdbd6f723b7163eacbaadad18a67826e88cbfe3dce7f", + "format": 1 + }, + { + "name": "tests/sanity", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/sanity/extra", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/sanity/extra/extra-docs.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6c7fbc8a07fa803ce062387232eb0d0198a311f5347493f06c19a07aa45a9bf6", + "format": 1 + }, + { + "name": "tests/sanity/extra/extra-docs.json.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/extra/extra-docs.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0fbd87476e9c35e4c5feb31be4aa1e8fc6aebf0de13058e5a267879f741ec0bf", + "format": 1 + }, + { + "name": "tests/sanity/extra/licenses.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c111eb62fa6f1b6c8a1260e9ff06c8b76ef8244428c6289969d79678093618f", + "format": 1 + }, + { + "name": "tests/sanity/extra/licenses.json.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/extra/licenses.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ff502f0707adf15a57b2fc48842fc47d154bfbd3aeadf4c0c05e96b0589c3cd4", + "format": 1 + }, + { + "name": "tests/sanity/extra/licenses.py.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "88f745b5d91e1371369c207e3392877af6f3e1de48fbaca63a728d4dcf79e03c", + "format": 1 + }, + { + "name": "tests/sanity/extra/no-unwanted-files.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a3d3b17f699b042958c7cd845a9d685bc935d83062e0bcf077f2c7200e2c0bac", + "format": 1 + }, + { + "name": "tests/sanity/extra/no-unwanted-files.json.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/extra/no-unwanted-files.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c0a3780ef0497719277e3dec9c3902572d7904db1a83953e0a46be8275729600", + "format": 1 + }, + { + "name": "tests/sanity/extra/update-docs.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "554b0924cd11998ef9b65661ff032e58df21b4b26b8674612f16a1b531d97de9", + "format": 1 + }, + { + "name": "tests/sanity/extra/update-docs.json.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/extra/update-docs.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8a3b6dbaf28d920c9d446af30a27be66d80583fd5af57d436b821d327b5cc682", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.10.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b8ef3a5984c970b2097edc50d6e287f010479ee9d1262c24e5f494339abd60ac", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.10.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.11.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b8ef3a5984c970b2097edc50d6e287f010479ee9d1262c24e5f494339abd60ac", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.11.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.12.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d78d96c9162b661df97409e019537d2f6cac6822e3d4faee844b54b1be907b22", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.12.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.13.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d78d96c9162b661df97409e019537d2f6cac6822e3d4faee844b54b1be907b22", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.13.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.14.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d78d96c9162b661df97409e019537d2f6cac6822e3d4faee844b54b1be907b22", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.14.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.15.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d78d96c9162b661df97409e019537d2f6cac6822e3d4faee844b54b1be907b22", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.15.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.16.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d78d96c9162b661df97409e019537d2f6cac6822e3d4faee844b54b1be907b22", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.16.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.9.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b8ef3a5984c970b2097edc50d6e287f010479ee9d1262c24e5f494339abd60ac", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.9.txt.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/compat", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/compat/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/compat/builtins.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1a66bf5868ec79d871566ca33e62612c7467681405a2c8aef8a93a768c3deebb", + "format": 1 + }, + { + "name": "tests/unit/compat/mock.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3bd66497039ee0b8bb9bbd7e1bade6ed20fd641ad262e99dab53b879e2173d83", + "format": 1 + }, + { + "name": "tests/unit/compat/unittest.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9db3b735dd4bde864e6c9d0f18a5a487d336bb4b60fd6b83088d72b4b05a021c", + "format": 1 + }, + { + "name": "tests/unit/plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test__api_data.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ab4f09d9aef04cc60d95ae745e6a549d8c2c1d1b43f4b26a11bb986b06aa169d", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test_quoting.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "12376c06f019c64642d4281e3709607047d0a9cdb71323d25b8d94fb0c8d399c", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/export", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8d21b42e87f3b1538f0a4ea558d2ec93516210cdea994f8652faac274635d0fb", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/export.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/export_verbose", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f62326680b3affcfad5718fe4db6b6aae213b53eb99c48c49fd7e6ca0d2d7bfd", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/export_verbose.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eef2936c689ad9f1f5fb6e21a4acb14ab3d8f70162b8ad7494adaf66ac587f7e", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e6a4983f12ede3b9995ffcfb59d711b89b38745c9084de0c863175f5f2532b0f", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0f6558742320457a85313a978aa58834d2e0a915e3a4c86ade9e587fe65c9459", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9b92f4b196df00c1aa7fb006b3e3e637912a3c07feae82855a6a75ca4cc9dc83", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "41eeef5f976ce4d413c4e3b42a88c877704a7316b1b0db0dee41b4f392d959a8", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "11a84f884f00b8aaab8bf53f65468f002af838c2a08e08bee1ca548fe3a1f6f3", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cf5c61d0c54d4583e46ff6e198c94ba28cca895d782d3b3e217b0c29bc86597a", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dc57eef1e65d31f99813f6d791941d191c61ad7855dd5911daca563793d230eb", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8803159ba6080f60a29513e1dd15472044ec419eb430f59f44df165d77a002db", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "513e2c73df0909f6e0d3436b1f814a2f0627517a216daf1434f215a6a0bfcbf0", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "532f9999b20ca5b30c339e515d6938a27fa3ce7597358692a0326793257cd312", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "67979768c0d45173cab664b8cf291701244f8a90c510c246164ed6e36abd62a0", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0446e354c094bea8ba33c4bdee11a9c0fc9e09aee11a0e6d92ef58fd59ee7d0e", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bee309db19bcdd458729b1f27825242a85ecb79f614d0b3ee7afc15f09240abe", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/system_package_print", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f8110c29ce0c00b77de46340820369a7f2b55095e760e62a0f5b0367427faee8", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/system_package_print.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/system_resource_print", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4552a30ac27b98cdb531164e6e945d66ce3bff0ddd196aa6dc9ae488bee324d7", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fixtures/system_resource_print.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/fake_api.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e1a0e7c26d139c804edad990a43d5c241f9b8832ac6edd7e9043123b57b6d231", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/routeros_module.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3a46965893b80d4f91c86e61c02ddff5f7899cbe29bc559774585e752425c39", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_api.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "96f21ea61d0c51d85585cd14da97086a4681c6eb509c7da032c0ddd1686222d4", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_api_facts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aacb275cf024fe8db4116de7b0fb0339234a6ac55b21bd0cfd9cbb42c9556fa6", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_api_find_and_modify.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a56e613e9fd26ec90ec72f25c15a7ea8ab7ab99dbccffc92c6251226443cffa1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_api_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5acc54f1fb618b84431bb94b626718f7faea823704e2e40b73ac0cc22ae0452c", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_api_modify.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8903880a669f9d4a33cb9ba6247ac99d7b94fd7ab7e712d189dfc4ee2b1cda87", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_command.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c7f40fef2fb28b1cf2bca83c0c08b0cc8dcbd3c84662af596de9268f1fb41db1", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_facts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b0f6a904901434a558dfe5ad3b765922f410f51c1ca1eb53164472073c4aba90", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "502774011756118495eb83f59e645e8e2fd965e180fb3e9a70115733eb3fd44a", + "format": 1 + }, + { + "name": "tests/unit/requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "def66f2fb171e83b8acd587d72826ae3c110839775c215aa28fcba5ace26bb83", + "format": 1 + }, + { + "name": "tests/unit/requirements.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "577b55fc276c5bcd3d0afdbd6f723b7163eacbaadad18a67826e88cbfe3dce7f", + "format": 1 + }, + { + "name": "tests/config.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "498d46cf08b5abb09880088fa8fd142315f7f775e94cdc7d41364ed20fc2cd65", + "format": 1 + }, + { + "name": "CHANGELOG.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7f88e8738859c0d903b42d2a5dcada13a7fd01fb3269ace2974666e0817e2e82", + "format": 1 + }, + { + "name": "CHANGELOG.rst.license", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6eb915239f9f35407fa68fdc41ed6522f1fdcce11badbdcd6057548023179ac1", + "format": 1 + }, + { + "name": "COPYING", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986", + "format": 1 + }, + { + "name": "README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "025ee8cc4b894b4a0b87c35b4cef0c747554d44b222c3a4b71344ce8150a49b7", + "format": 1 + }, + { + "name": "codecov.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "97e871facc3d67507644229e7571da7a8caca083c25cad95fcb5ba1c48076040", + "format": 1 + }, + { + "name": "update-docs.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bd643824d0b7a1ef65d4760c32f1a38ef37dc7c2b4ec55eec76245164950cbb5", + "format": 1 + } + ], + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/community/routeros/LICENSES/BSD-2-Clause.txt b/ansible_collections/community/routeros/LICENSES/BSD-2-Clause.txt new file mode 100644 index 000000000..6810e04e3 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/LICENSES/GPL-3.0-or-later.txt b/ansible_collections/community/routeros/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/ansible_collections/community/routeros/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/ansible_collections/community/routeros/LICENSES/PSF-2.0.txt b/ansible_collections/community/routeros/LICENSES/PSF-2.0.txt new file mode 100644 index 000000000..35acd7fb5 --- /dev/null +++ b/ansible_collections/community/routeros/LICENSES/PSF-2.0.txt @@ -0,0 +1,48 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/ansible_collections/community/routeros/MANIFEST.json b/ansible_collections/community/routeros/MANIFEST.json new file mode 100644 index 000000000..c5afc614b --- /dev/null +++ b/ansible_collections/community/routeros/MANIFEST.json @@ -0,0 +1,38 @@ +{ + "collection_info": { + "namespace": "community", + "name": "routeros", + "version": "2.8.2", + "authors": [ + "Egor Zaitsev (github.com/heuels)", + "Nikolay Dachev (github.com/NikolayDachev)", + "Felix Fontein (github.com/felixfontein)" + ], + "readme": "README.md", + "tags": [ + "network", + "mikrotik", + "routeros" + ], + "description": "Modules for MikroTik RouterOS", + "license": [ + "GPL-3.0-or-later" + ], + "license_file": null, + "dependencies": { + "ansible.netcommon": ">=1.0.0" + }, + "repository": "https://github.com/ansible-collections/community.routeros", + "documentation": "https://docs.ansible.com/ansible/devel/collections/community/routeros/", + "homepage": "https://github.com/ansible-collections/community.routeros", + "issues": "https://github.com/ansible-collections/community.routeros/issues" + }, + "file_manifest_file": { + "name": "FILES.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fa74768aa74fe395b07ca0059809ecea66739b21107d3a9e7ce2c81a45393211", + "format": 1 + }, + "format": 1 +}
\ No newline at end of file diff --git a/ansible_collections/community/routeros/README.md b/ansible_collections/community/routeros/README.md new file mode 100644 index 000000000..7378c34a0 --- /dev/null +++ b/ansible_collections/community/routeros/README.md @@ -0,0 +1,190 @@ +<!-- +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 +--> + +# 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. + +You can find [documentation for the modules and plugins in this collection here](https://docs.ansible.com/ansible/devel/collections/community/routeros/). + +## Tested with Ansible + +Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13, and ansible-core 2.14 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported. + +## External requirements + +The exact requirements for every module are listed in the module documentation. + +### Supported connections + +The collection supports the `network_cli` connection. + +### Edge cases + +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` + +You can find [documentation for the modules and plugins in this collection here](https://docs.ansible.com/ansible/devel/collections/community/routeros/). + +## Using this collection + +See [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for general detail on using collections. + +There are two approaches for using this collection. The `command` and `facts` modules use the `network_cli` connection and connect with SSH. The `api` module connects with the HTTP/HTTPS API. + +### Prerequisites + +The terminal-based modules in this collection (`community.routeros.command` and `community.routeros.facts`) do not support arbitrary symbols in 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, the `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. + +```yaml +- community.routeros.command: + commands: + - /ip + - print +``` + +### Connecting with `network_cli` + +Example inventory `hosts` file: + +```.ini +[routers] +router ansible_host=192.168.1.1 + +[routers:vars] +ansible_connection=ansible.netcommon.network_cli +ansible_network_os=community.routeros.routeros +ansible_user=admin +ansible_ssh_pass=test1234 +``` + +Example playbook: + +```.yaml +--- +- name: RouterOS test with network_cli connection + 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 + + - name: Retrieve facts + community.routeros.facts: + - ansible.builtin.debug: + msg: "First IP address: {{ ansible_net_all_ipv4_addresses[0] }}" +``` + +### Connecting with HTTP/HTTPS API + +Example playbook: + +```.yaml +--- +- name: RouterOS test with API + hosts: localhost + 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: + 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 + +We're following the general Ansible contributor guidelines; see [Ansible Community Guide](https://docs.ansible.com/ansible/latest/community/index.html). + +If you want to clone this repositority (or a fork of it) to improve it, you can proceed as follows: +1. Create a directory `ansible_collections/community`; +2. In there, checkout this repository (or a fork) as `routeros`; +3. Add the directory containing `ansible_collections` to your [ANSIBLE_COLLECTIONS_PATH](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths). + +See [Ansible's dev guide](https://docs.ansible.com/ansible/devel/dev_guide/developing_collections.html#contributing-to-collections) for more information. + +## Release notes + +See the [changelog](https://github.com/ansible-collections/community.routeros/blob/main/CHANGELOG.rst). + +## Roadmap + +We plan to regularly release minor and patch versions, whenever new features are added or bugs fixed. Our collection follows [semantic versioning](https://semver.org/), so breaking changes will only happen in major releases. + +## More information + +- [Ansible Collection overview](https://github.com/ansible-collections/overview) +- [Ansible User guide](https://docs.ansible.com/ansible/latest/user_guide/index.html) +- [Ansible Developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html) +- [Ansible Collections Checklist](https://github.com/ansible-collections/overview/blob/master/collection_requirements.rst) +- [Ansible Community code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) +- [The Bullhorn (the Ansible Contributor newsletter)](https://us19.campaign-archive.com/home/?u=56d874e027110e35dea0e03c1&id=d6635f5420) +- [Changes impacting Contributors](https://github.com/ansible-collections/overview/issues/45) + +## Licensing + +This collection is primarily licensed and distributed as a whole under the GNU General Public License v3.0 or later. + +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/dep5`. This conforms to the [REUSE specification](https://reuse.software/spec/). diff --git a/ansible_collections/community/routeros/changelogs/changelog.yaml b/ansible_collections/community/routeros/changelogs/changelog.yaml new file mode 100644 index 000000000..e09fa3e38 --- /dev/null +++ b/ansible_collections/community/routeros/changelogs/changelog.yaml @@ -0,0 +1,449 @@ +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). + 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 + 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). + release_summary: Small improvements and bugfixes over the initial release. + fragments: + - 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). + 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 + 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). + release_summary: Maintenance release with a bugfix for ``api``. + fragments: + - 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). + release_summary: This release allow dashes in usernames for SSH-based modules. + fragments: + - 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). + 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). + 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 + release_date: '2021-06-29' + 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.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).' + 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 + 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.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 <https://reuse.software/spec/>`__ + 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 implemeted for ROS6 due to the significat 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' diff --git a/ansible_collections/community/routeros/changelogs/changelog.yaml.license b/ansible_collections/community/routeros/changelogs/changelog.yaml.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/changelogs/config.yaml b/ansible_collections/community/routeros/changelogs/config.yaml new file mode 100644 index 000000000..70a1f6701 --- /dev/null +++ b/ansible_collections/community/routeros/changelogs/config.yaml @@ -0,0 +1,34 @@ +--- +# 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 +keep_fragments: false +mention_ancestor: true +flatmap: true +new_plugins_after_name: removed_features +notesdir: fragments +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 +title: Community RouterOS diff --git a/ansible_collections/community/routeros/changelogs/fragments/.keep b/ansible_collections/community/routeros/changelogs/fragments/.keep new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/routeros/changelogs/fragments/.keep diff --git a/ansible_collections/community/routeros/codecov.yml b/ansible_collections/community/routeros/codecov.yml new file mode 100644 index 000000000..3b2f9ed6b --- /dev/null +++ b/ansible_collections/community/routeros/codecov.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 + +fixes: + - "ansible_collections/community/routeros/::" diff --git a/ansible_collections/community/routeros/docs/docsite/extra-docs.yml b/ansible_collections/community/routeros/docs/docsite/extra-docs.yml new file mode 100644 index 000000000..609159123 --- /dev/null +++ b/ansible_collections/community/routeros/docs/docsite/extra-docs.yml @@ -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 + +sections: + - title: Guides + toctree: + - api-guide + - ssh-guide + - quoting diff --git a/ansible_collections/community/routeros/docs/docsite/links.yml b/ansible_collections/community/routeros/docs/docsite/links.yml new file mode 100644 index 000000000..9da799e7f --- /dev/null +++ b/ansible_collections/community/routeros/docs/docsite/links.yml @@ -0,0 +1,27 @@ +--- +# 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: 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' + mailing_lists: + - topic: Ansible Project List + url: https://groups.google.com/g/ansible-project diff --git a/ansible_collections/community/routeros/docs/docsite/rst/api-guide.rst b/ansible_collections/community/routeros/docs/docsite/rst/api-guide.rst new file mode 100644 index 000000000..f3bb6295b --- /dev/null +++ b/ansible_collections/community/routeros/docs/docsite/rst/api-guide.rst @@ -0,0 +1,198 @@ +.. + 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 <ansible_collections.community.routeros.api_module>` to connect to a RouterOS device with the RouterOS API. More specific module to modify certain entries are the :ref:`community.routeros.api_modify <ansible_collections.community.routeros.api_modify_module>` and :ref:`community.routeros.api_find_and_modify <ansible_collections.community.routeros.api_find_and_modify_module>` modules. The :ref:`community.routeros.api_info module <ansible_collections.community.routeros.api_info_module>` allows to retrieve information on specific predefined paths that can be used as input for the ``community.routeros.api_modify`` module, and the :ref:`community.routeros.api_facts module <ansible_collections.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``: + +.. code-block:: yaml+jinja + + --- + - name: RouterOS test with API + hosts: localhost + gather_facts: false + vars: + hostname: 192.168.1.1 + username: admin + password: test1234 + tasks: + - name: Get "ip address print" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + # 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 + register: print_path + + - name: Show IP address of first interface + ansible.builtin.debug: + msg: "{{ print_path.msg[0].address }}" + +This results in the following output: + +.. code-block:: ansible-output + + PLAY [RouterOS test] ********************************************************************************************* + + TASK [Get "ip address print"] ************************************************************************************ + ok: [localhost] + + TASK [Show IP address of first interface] ************************************************************************ + ok: [localhost] => { + "msg": "192.168.2.1/24" + } + + PLAY RECAP ******************************************************************************************************* + 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 <ansible_collections.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`` 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: + +:force_no_cert: Setting to ``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 ``false``. +: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. + +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. + +Setting up a PKI +^^^^^^^^^^^^^^^^ + +Please follow the instructions in the ``community.crypto`` :ref:`ansible_collections.community.crypto.docsite.guide_ownca` guide to set up a CA certificate and sign a certificate for your router. You should add a Subject Alternative Name for the IP address (for example ``IP:192.168.1.1``) and - if available - for the DNS name (for example ``DNS:router.local``) to the certificate. + +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 <https://en.wikipedia.org/wiki/PKCS_12>`_. This can be done with the :ref:`community.crypto.openssl_pkcs12 <ansible_collections.community.crypto.openssl_pkcs12_module>`. 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 + + --- + - name: Install certificates on devices + hosts: routers + gather_facts: false + tasks: + - block: + - set_fact: + random_password: "{{ lookup('community.general.random_string', length=32, override_all='0123456789abcdefghijklmnopqrstuvwxyz') }}" + + - name: Create PKCS#12 bundle + openssl_pkcs12: + path: keys/{{ inventory_hostname }}.p12 + certificate_path: keys/{{ inventory_hostname }}.pem + privatekey_path: keys/{{ inventory_hostname }}.key + friendly_name: '{{ inventory_hostname }}' + passphrase: "{{ random_password }}" + mode: "0600" + changed_when: false + delegate_to: localhost + + - name: Copy router certificate onto router + ansible.netcommon.net_put: + src: 'keys/{{ inventory_hostname }}.p12' + dest: '{{ inventory_hostname }}.p12' + + - name: Install router certificate and clean up + community.routeros.command: + commands: + # Import certificate: + - /certificate import name={{ inventory_hostname }} file-name={{ inventory_hostname }}.p12 passphrase="{{ random_password }}" + # Remove PKCS12 bundle: + - /file remove {{ inventory_hostname }}.p12 + # Show certificates + - /certificate print + register: output + + - name: Show result of certificate import + debug: + var: output.stdout_lines[0] + + - name: Show certificates + debug: + var: output.stdout_lines[2] + + always: + - name: Wipe PKCS12 bundle + command: wipe keys/{{ inventory_hostname }}.p12 + changed_when: false + delegate_to: localhost + + - name: Use certificate + community.routeros.command: + commands: + - /ip service set www-ssl address={{ admin_network }} certificate={{ inventory_hostname }} disabled=no tls-version=only-1.2 + - /ip service set api-ssl address={{ admin_network }} certificate={{ inventory_hostname }} tls-version=only-1.2 + +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 <ansible_collections.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 + ca_path: /path/to/ca-certificate.pem diff --git a/ansible_collections/community/routeros/docs/docsite/rst/quoting.rst b/ansible_collections/community/routeros/docs/docsite/rst/quoting.rst new file mode 100644 index 000000000..3091fc857 --- /dev/null +++ b/ansible_collections/community/routeros/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 :ref:`community.routeros.command module <ansible_collections.community.routeros.command_module>` or the :ref:`community.routeros.api module <ansible_collections.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 :ref:`community.routeros.quote_argument_value filter <ansible_collections.community.routeros.quote_argument_value_filter>` quotes an argument value: ``'this is a "comment"' | community.routeros.quote_argument_value == '"this is a \\"comment\\""'``. +- The :ref:`community.routeros.quote_argument filter <ansible_collections.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 :ref:`community.routeros.join filter <ansible_collections.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 :ref:`community.routeros.split filter <ansible_collections.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 :ref:`community.routeros.list_to_dict filter <ansible_collections.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: ``require_assignment`` (default value ``true``) allows to accept arguments without values when set to ``false``; and ``skip_empty_values`` (default value ``false``) allows to skip arguments whose value is empty. diff --git a/ansible_collections/community/routeros/docs/docsite/rst/ssh-guide.rst b/ansible_collections/community/routeros/docs/docsite/rst/ssh-guide.rst new file mode 100644 index 000000000..bdbdbfe84 --- /dev/null +++ b/ansible_collections/community/routeros/docs/docsite/rst/ssh-guide.rst @@ -0,0 +1,127 @@ +.. + 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 +=========================================== + +The collection offers two modules to connect to RouterOS devies with SSH: + +- The :ref:`community.routeros.facts module <ansible_collections.community.routeros.facts_module>` gathers facts about a RouterOS device; +- The :ref:`community.routeros.command module <ansible_collections.community.routeros.command_module>` executes commands on a RouterOS device. + +The modules need the :ref:`ansible.netcommon.network_cli connection plugin <ansible_collections.ansible.netcommon.network_cli_connection>` 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 <https://github.com/ansible-collections/community.routeros/issues/31>`__). Similar problems can happen for unsupported characters in your username. + +2. The :ref:`community.routeros.command module <ansible_collections.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 + + - community.routeros.command: + commands: + - /ip + - print + +3. When using the :ref:`community.routeros.command module <ansible_collections.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 <https://github.com/ansible-collections/community.routeros/issues/6>`__). + +4. Finally, the :ref:`ansible.netcommon.network_cli connection plugin <ansible_collections.ansible.netcommon.network_cli_connection>` uses `paramiko <https://pypi.org/project/paramiko/>`_ by default to connect to devices with SSH. You can set its ``ssh_type`` option to ``libssh`` to use `ansible-pylibssh <https://pypi.org/project/ansible-pylibssh/>`_ instead, which offers Python bindings to libssh. See its documentation for details. + +Setting up an inventory +----------------------- + +An example inventory ``hosts`` file for a RouterOS device is as follows: + +.. code-block:: ini + + [routers] + router ansible_host=192.168.2.1 + + [routers:vars] + ansible_connection=ansible.netcommon.network_cli + ansible_network_os=community.routeros.routeros + 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 <ansible_collections.ansible.netcommon.network_cli_connection>` together with the the :ref:`community.routeros.routeros cliconf plugin <ansible_collections.community.routeros.routeros_cliconf>`. The credentials are stored as ``ansible_user`` and ``ansible_ssh_pass`` in the inventory. + +Connecting to the device +------------------------ + +With the above inventory, you can use the following playbook to execute ``/system resource print`` on the device + +.. code-block:: yaml+jinja + + --- + - name: RouterOS test with network_cli connection + hosts: routers + gather_facts: false + tasks: + + - 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: Gather facts + community.routeros.facts: + + - name: Show a fact + debug: + msg: "First IP address: {{ ansible_net_all_ipv4_addresses[0] }}" + +This results in the following output: + +.. code-block:: ansible-output + + PLAY [RouterOS test with network_cli connection] ***************************************************************** + + TASK [Gather system resources] *********************************************************************************** + ok: [router] + + TASK [Show system resources] ************************************************************************************* + ok: [router] => { + "system_resource_print.stdout_lines": [ + [ + "uptime: 3d10h28m51s", + " version: 6.48.3 (stable)", + " build-time: May/25/2021 06:09:45", + " free-memory: 31.2MiB", + " total-memory: 64.0MiB", + " cpu: MIPS 24Kc V7.4", + " cpu-count: 1", + " cpu-frequency: 400MHz", + " cpu-load: 1%", + " free-hdd-space: 54.2MiB", + " total-hdd-space: 128.0MiB", + " write-sect-since-reboot: 927", + " write-sect-total: 51572981", + " bad-blocks: 1%", + " architecture-name: mipsbe", + " board-name: RB750GL", + " platform: MikroTik" + ] + ] + } + + TASK [Gather facts] ********************************************************************************************** + ok: [router] + + TASK [Show a fact] *********************************************************************************************** + ok: [router] => { + "msg": "First IP address: 192.168.2.1" + } + + PLAY RECAP ******************************************************************************************************* + router : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 diff --git a/ansible_collections/community/routeros/meta/ee-requirements.txt b/ansible_collections/community/routeros/meta/ee-requirements.txt new file mode 100644 index 000000000..a36140cc1 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/meta/execution-environment.yml b/ansible_collections/community/routeros/meta/execution-environment.yml new file mode 100644 index 000000000..ac7ebac8a --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/meta/runtime.yml b/ansible_collections/community/routeros/meta/runtime.yml new file mode 100644 index 000000000..5aafd31d0 --- /dev/null +++ b/ansible_collections/community/routeros/meta/runtime.yml @@ -0,0 +1,13 @@ +--- +# 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.9.10' +action_groups: + api: + - api + - api_facts + - api_find_and_modify + - api_info + - api_modify diff --git a/ansible_collections/community/routeros/plugins/cliconf/routeros.py b/ansible_collections/community/routeros/plugins/cliconf/routeros.py new file mode 100644 index 000000000..412627b8e --- /dev/null +++ b/ansible_collections/community/routeros/plugins/cliconf/routeros.py @@ -0,0 +1,62 @@ +# 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 = ''' +--- +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. +''' + +import re +import json + +from ansible.module_utils.common.text.converters import to_text +from ansible.plugins.cliconf import CliconfBase + + +class Cliconf(CliconfBase): + + def get_device_info(self): + device_info = {} + device_info['network_os'] = 'RouterOS' + + resource = self.get('/system resource print') + data = to_text(resource, errors='surrogate_or_strict').strip() + match = re.search(r'version: (\S+)', data) + if match: + device_info['network_os_version'] = match.group(1) + + routerboard = self.get('/system routerboard print') + data = to_text(routerboard, errors='surrogate_or_strict').strip() + match = re.search(r'model: (.+)$', data, re.M) + if match: + device_info['network_os_model'] = match.group(1) + + identity = self.get('/system identity print') + data = to_text(identity, errors='surrogate_or_strict').strip() + match = re.search(r'name: (.+)$', data, re.M) + if match: + device_info['network_os_hostname'] = match.group(1) + + return device_info + + def get_config(self, source='running', flags=None, format=None): + return + + def edit_config(self, command): + return + + def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False): + return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all) + + def get_capabilities(self): + result = super(Cliconf, self).get_capabilities() + return json.dumps(result) diff --git a/ansible_collections/community/routeros/plugins/doc_fragments/api.py b/ansible_collections/community/routeros/plugins/doc_fragments/api.py new file mode 100644 index 000000000..dea374b95 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/doc_fragments/api.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Nikolay Dachev <nikolay@dachev.info> +# 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 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 + force_no_cert: + description: + - Set to C(true) to connect without a certificate when I(tls=true). + - See also I(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 + I(validate_certs) and I(ca_path). + type: bool + default: false + version_added: 2.4.0 + 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 + encoding: + description: + - Use the specified encoding when communicating with the RouterOS device. + - Default is C(ASCII). Note that C(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 +''' diff --git a/ansible_collections/community/routeros/plugins/doc_fragments/attributes.py b/ansible_collections/community/routeros/plugins/doc_fragments/attributes.py new file mode 100644 index 000000000..e18a48ff2 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/doc_fragments/attributes.py @@ -0,0 +1,98 @@ +# -*- 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 +''' + + # 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/ansible_collections/community/routeros/plugins/filter/join.yml b/ansible_collections/community/routeros/plugins/filter/join.yml new file mode 100644 index 000000000..9ff8a50f1 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/filter/join.yml @@ -0,0 +1,31 @@ +--- +# 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/ansible_collections/community/routeros/plugins/filter/list_to_dict.yml b/ansible_collections/community/routeros/plugins/filter/list_to_dict.yml new file mode 100644 index 000000000..920414ced --- /dev/null +++ b/ansible_collections/community/routeros/plugins/filter/list_to_dict.yml @@ -0,0 +1,41 @@ +--- +# 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 C(community.routeros.split) filter. + type: list + elements: string + required: true + require_assignment: + description: + - Allows to accept arguments without values when set to C(false). + type: boolean + default: true + skip_empty_values: + description: + - Allows to skip arguments whose value is empty when set to C(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/ansible_collections/community/routeros/plugins/filter/quote_argument.yml b/ansible_collections/community/routeros/plugins/filter/quote_argument.yml new file mode 100644 index 000000000..26a1f0401 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/filter/quote_argument.yml @@ -0,0 +1,30 @@ +--- +# 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/ansible_collections/community/routeros/plugins/filter/quote_argument_value.yml b/ansible_collections/community/routeros/plugins/filter/quote_argument_value.yml new file mode 100644 index 000000000..839895bc9 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/filter/quote_argument_value.yml @@ -0,0 +1,30 @@ +--- +# 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/ansible_collections/community/routeros/plugins/filter/quoting.py b/ansible_collections/community/routeros/plugins/filter/quoting.py new file mode 100644 index 000000000..3985d5581 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/filter/quoting.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein <felix@fontein.de> +# 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/ansible_collections/community/routeros/plugins/filter/split.yml b/ansible_collections/community/routeros/plugins/filter/split.yml new file mode 100644 index 000000000..5fc4b30c7 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/filter/split.yml @@ -0,0 +1,31 @@ +--- +# 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/ansible_collections/community/routeros/plugins/module_utils/_api_data.py b/ansible_collections/community/routeros/plugins/module_utils/_api_data.py new file mode 100644 index 000000000..59e5b5c52 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/module_utils/_api_data.py @@ -0,0 +1,2860 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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 + + +class APIData(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): + 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 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)) + + +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): + 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') + 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 + + +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', 'bonding'): APIData( + 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( + fully_understood=True, + primary_keys=('name', ), + 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), + 'frame-types': KeyInfo(default='admit-all'), + 'forward-delay': KeyInfo(default='15s'), + 'igmp-snooping': KeyInfo(default=False), + 'ingress-filtering': KeyInfo(default=True), + 'max-message-age': KeyInfo(default='20s'), + 'mtu': KeyInfo(default='auto'), + '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( + 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( + 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), + 'full-duplex': KeyInfo(default=True), + 'l2mtu': KeyInfo(default=1598), + '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), + 'poe-priority': KeyInfo(can_disable=True), + '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='95C'), + 'speed': KeyInfo(), + 'tx-flow-control': KeyInfo(default='off'), + }, + ), + ('interface', 'ethernet', 'poe'): APIData( + 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( + 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( + 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( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'exclude': KeyInfo(), + 'include': KeyInfo(), + 'name': KeyInfo(), + }, + ), + ('interface', 'list', 'member'): APIData( + 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( + 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', 'pppoe-client'): APIData( + 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( + 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( + 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), + }, + ), + ('interface', 'wireless', 'security-profiles'): APIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'authentication-types': KeyInfo(), + 'disable-pmkid': KeyInfo(), + 'eap-methods': KeyInfo(), + 'group-ciphers': KeyInfo(), + 'group-key-update': KeyInfo(), + 'interim-update': KeyInfo(), + 'management-protection': KeyInfo(), + 'management-protection-key': KeyInfo(), + 'mode': KeyInfo(), + 'mschapv2-password': KeyInfo(), + 'mschapv2-username': KeyInfo(), + 'name': KeyInfo(), + 'radius-called-format': KeyInfo(), + 'radius-eap-accounting': KeyInfo(), + 'radius-mac-accounting': KeyInfo(), + 'radius-mac-authentication': KeyInfo(), + 'radius-mac-caching': KeyInfo(), + 'radius-mac-format': KeyInfo(), + 'radius-mac-mode': KeyInfo(), + 'static-algo-0': KeyInfo(), + 'static-algo-1': KeyInfo(), + 'static-algo-2': KeyInfo(), + 'static-algo-3': KeyInfo(), + 'static-key-0': KeyInfo(), + 'static-key-1': KeyInfo(), + 'static-key-2': KeyInfo(), + 'static-key-3': KeyInfo(), + 'static-sta-private-algo': KeyInfo(), + 'static-sta-private-key': KeyInfo(), + 'static-transmit-key': KeyInfo(), + 'supplicant-identity': KeyInfo(), + 'tls-certificate': KeyInfo(), + 'tls-mode': KeyInfo(), + 'unicast-ciphers': KeyInfo(), + 'wpa-pre-shared-key': KeyInfo(), + 'wpa2-pre-shared-key': KeyInfo(), + }, + ), + ('ip', 'hotspot', 'profile'): APIData( + 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( + 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( + 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( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'name': KeyInfo(), + 'responder': KeyInfo(), + 'use-responder-dns': KeyInfo(), + }, + ), + ('ip', 'ipsec', 'peer'): APIData( + 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( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'name': KeyInfo(), + }, + ), + ('ip', 'ipsec', 'profile'): APIData( + 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( + 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( + fully_understood=True, + primary_keys=('name', ), + fields={ + 'name': KeyInfo(), + 'ranges': KeyInfo(), + }, + ), + ('ip', 'route'): APIData( + 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', 'vrf'): APIData( + fully_understood=True, + primary_keys=('routing-mark', ), + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(default=False), + 'interfaces': KeyInfo(), + 'routing-mark': KeyInfo(), + }, + ), + ('ip', 'dhcp-server'): APIData( + 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), + }, + ), + ('routing', 'ospf', 'instance'): APIData( + 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( + 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( + 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( + 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-v3', 'instance'): APIData( + 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( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'area-id': KeyInfo(), + 'disabled': KeyInfo(), + 'instance': KeyInfo(), + 'name': KeyInfo(), + 'type': KeyInfo(), + }, + ), + ('routing', 'pimsm', 'instance'): APIData( + 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( + 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), + }, + ), + ('snmp', 'community'): APIData( + fully_understood=True, + primary_keys=('name', ), + fields={ + '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( + 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( + 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(), + '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', 'configuration'): APIData( + 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( + 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( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'disabled': KeyInfo(), + 'forbid': KeyInfo(), + 'interface': KeyInfo(), + }, + ), + ('caps-man', 'provisioning'): APIData( + 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( + 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( + 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( + fully_understood=True, + primary_keys=('interface', ), + 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), + 'ingress-filtering': 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( + single_value=True, + fully_understood=True, + fields={ + 'bridge': KeyInfo(default='none'), + 'peer-port': KeyInfo(default='none'), + } + ), + ('interface', 'bridge', 'port-controller'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'bridge': KeyInfo(default='none'), + 'cascade-ports': KeyInfo(default=''), + 'switch': KeyInfo(default='none'), + }, + ), + ('interface', 'bridge', 'port-extender'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'control-ports': KeyInfo(default=''), + 'excluded-ports': KeyInfo(default=''), + 'switch': KeyInfo(default='none'), + }, + ), + ('interface', 'bridge', 'settings'): APIData( + 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( + 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( + 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( + single_value=True, + fully_understood=True, + fields={ + 'discover-interface-list': KeyInfo(), + 'lldp-med-net-policy-vlan': KeyInfo(default='disabled'), + 'protocol': KeyInfo(default='cdp,lldp,mndp'), + }, + ), + ('ip', 'settings'): APIData( + single_value=True, + fully_understood=True, + 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( + 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(), + 'interface': KeyInfo(required=True), + 'no-dad': KeyInfo(default=False), + }, + ), + ('ipv6', 'settings'): APIData( + single_value=True, + fully_understood=True, + 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), + 'max-neighbor-entries': KeyInfo(default=8192), + }, + ), + ('interface', 'detect-internet'): APIData( + 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-server', 'server'): APIData( + 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-server', 'server'): APIData( + 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'), + 'netmask': KeyInfo(default=24), + 'port': KeyInfo(default=1194), + 'require-client-certificate': KeyInfo(default=False), + }, + ), + ('interface', 'pptp-server', 'server'): APIData( + 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( + 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', 'wireguard'): APIData( + 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( + 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(), + }, + ), + ('interface', 'wireless', 'align'): APIData( + 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', 'cap'): APIData( + 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', 'sniffer'): APIData( + 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( + single_value=True, + fully_understood=True, + fields={ + 'channel-time': KeyInfo(default='200ms'), + 'multiple-channels': KeyInfo(default=True), + 'receive-errors': KeyInfo(default=False), + }, + ), + ('ip', 'accounting'): APIData( + 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( + single_value=True, + fully_understood=True, + fields={ + 'accessible-via-web': KeyInfo(default=False), + 'address': KeyInfo(default='0.0.0.0/0'), + }, + ), + ('ip', 'address'): APIData( + 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( + 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( + single_value=True, + fully_understood=True, + fields={ + 'ddns-enabled': KeyInfo(default=False), + 'ddns-update-interval': KeyInfo(default='none'), + 'update-time': KeyInfo(default=True), + }, + ), + ('ip', 'cloud', 'advanced'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'use-local-address': KeyInfo(default=False), + }, + ), + ('ip', 'dhcp-client'): APIData( + 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-server', 'config'): APIData( + 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( + 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( + 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', 'dns'): APIData( + single_value=True, + fully_understood=True, + 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', 'static'): APIData( + fully_understood=True, + required_one_of=[['name', 'regexp']], + mutually_exclusive=[['name', 'regexp']], + 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( + 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( + fully_understood=True, + stratify_keys=('chain', ), + fields={ + 'action': 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-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(), + '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(), + 'layer7-protocol': KeyInfo(can_disable=True), + '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), + '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), + 'reject-with': KeyInfo(), + '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( + fully_understood=True, + stratify_keys=('chain', ), + fields={ + 'action': 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-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(), + '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(), + 'layer7-protocol': KeyInfo(can_disable=True), + 'limit': KeyInfo(can_disable=True), + 'log': KeyInfo(), + 'log-prefix': KeyInfo(), + '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), + 'passthrough': 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), + '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( + 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-type': KeyInfo(can_disable=True), + 'content': KeyInfo(can_disable=True), + 'disabled': KeyInfo(), + '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(), + 'layer7-protocol': KeyInfo(can_disable=True), + '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), + 'psd': KeyInfo(can_disable=True), + 'random': KeyInfo(can_disable=True), + 'realm': KeyInfo(can_disable=True), + 'routing-mark': KeyInfo(can_disable=True), + 'same-not-by-dst': 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-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( + 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(), + '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(), + '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), + '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( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'disabled': KeyInfo(), + 'name': KeyInfo(), + }, + ), + ('ip', 'ipsec', 'settings'): APIData( + 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( + 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( + 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( + 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( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'disabled': KeyInfo(), + 'name': KeyInfo(), + 'password': KeyInfo(), + 'read-only': KeyInfo(), + }, + ), + ('ip', 'socks'): APIData( + 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( + single_value=True, + fully_understood=True, + 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( + single_value=True, + fully_understood=True, + fields={ + 'max-block-size': KeyInfo(default=4096), + }, + ), + ('ip', 'traffic-flow'): APIData( + 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( + 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', 'upnp'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'allow-disable-external-interface': KeyInfo(default=False), + 'enabled': KeyInfo(default=False), + 'show-dummy-rule': KeyInfo(default=True), + }, + ), + ('ipv6', 'dhcp-client'): APIData( + 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), + }, + ), + ('ipv6', 'dhcp-server'): APIData( + 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( + fully_understood=True, + primary_keys=('name',), + fields={ + 'code': KeyInfo(required=True), + 'name': KeyInfo(), + 'value': KeyInfo(default=''), + }, + ), + ('ipv6', 'firewall', 'address-list'): APIData( + 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( + fully_understood=True, + stratify_keys=('chain', ), + fields={ + 'action': 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(), + '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), + '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( + 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(), + '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', 'raw'): APIData( + 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(), + '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( + 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', 'default'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'autonomous': KeyInfo(default=True), + 'preferred-lifetime': KeyInfo(default='1w'), + 'valid-lifetime': KeyInfo(default='4w2d'), + }, + ), + ('ipv6', 'route'): APIData( + 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(), + '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( + 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( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'disabled': KeyInfo(), + 'interface': KeyInfo(), + 'mpls-mtu': KeyInfo(), + }, + ), + ('mpls', 'ldp'): APIData( + 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), + }, + ), + ('port', 'firmware'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'directory': KeyInfo(default='firmware'), + 'ignore-directip-modem': KeyInfo(default=False), + }, + ), + ('ppp', 'aaa'): APIData( + 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', 'incoming'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'accept': KeyInfo(default=False), + 'port': KeyInfo(default=3799), + }, + ), + ('routing', 'bfd', 'interface'): APIData( + unknown_mechanism=True, + # primary_keys=('default', ), + fields={ + 'default': KeyInfo(), + 'disabled': KeyInfo(), + 'interface': KeyInfo(), + 'interval': KeyInfo(), + 'min-rx': KeyInfo(), + 'multiplier': KeyInfo(), + }, + ), + ('routing', 'mme'): APIData( + 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( + 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( + 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( + single_value=True, + fully_understood=True, + fields={ + 'contact': KeyInfo(default=''), + 'enabled': KeyInfo(default=False), + 'engine-id': KeyInfo(default=''), + '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=''), + }, + ), + ('system', 'clock'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'time-zone-autodetect': KeyInfo(default=True), + 'time-zone-name': KeyInfo(default='manual'), + }, + ), + ('system', 'clock', 'manual'): APIData( + 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', 'identity'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'name': KeyInfo(default='Mikrotik'), + }, + ), + ('system', 'leds', 'settings'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'all-leds-off': KeyInfo(default='never'), + }, + ), + ('system', 'note'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'note': KeyInfo(default=''), + 'show-at-login': KeyInfo(default=True), + }, + ), + ('system', 'ntp', 'client'): APIData( + 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( + 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( + 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( + single_value=True, + fully_understood=True, + fields={ + 'channel': KeyInfo(default='stable'), + }, + ), + ('system', 'routerboard', 'settings'): APIData( + 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'), + 'enable-jumper-reset': KeyInfo(default=True), + 'enter-setup-on': KeyInfo(default='any-key'), + 'force-backup-booter': KeyInfo(default=False), + '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( + 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( + 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( + 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( + 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( + 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=''), + }, + ), + ('tool', 'graphing'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'page-refresh': KeyInfo(default=300), + 'store-every': KeyInfo(default='5min'), + }, + ), + ('tool', 'mac-server'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'allowed-interface-list': KeyInfo(), + }, + ), + ('tool', 'mac-server', 'mac-winbox'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'allowed-interface-list': KeyInfo(), + }, + ), + ('tool', 'mac-server', 'ping'): APIData( + single_value=True, + fully_understood=True, + fields={ + 'enabled': KeyInfo(default=True), + }, + ), + ('tool', 'romon'): APIData( + 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( + fields={ + 'cost': KeyInfo(), + 'disabled': KeyInfo(), + 'forbid': KeyInfo(), + 'interface': KeyInfo(), + 'secrets': KeyInfo(), + }, + ), + ('tool', 'sms'): APIData( + 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( + 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( + 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', 'aaa'): APIData( + 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), + }, + ), + ('queue', 'interface'): APIData( + primary_keys=('interface', ), + fully_understood=True, + fixed_entries=True, + fields={ + 'interface': KeyInfo(required=True), + 'queue': KeyInfo(required=True), + }, + ), + ('queue', 'tree'): APIData( + 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'), + }, + ), + ('interface', 'ethernet', 'switch'): APIData( + 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( + 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'), + }, + ), + ('ip', 'dhcp-client', 'option'): APIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'code': KeyInfo(), + 'name': KeyInfo(), + 'value': KeyInfo(), + }, + ), + ('ppp', 'profile'): APIData( + has_identifier=True, + fields={ + 'address-list': KeyInfo(), + 'bridge': KeyInfo(can_disable=True), + 'bridge-horizon': KeyInfo(can_disable=True), + 'bridge-learning': KeyInfo(), + 'bridge-path-cost': KeyInfo(can_disable=True), + 'bridge-port-priority': KeyInfo(can_disable=True), + 'change-tcp-mss': KeyInfo(), + '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(), + 'on-down': KeyInfo(), + 'on-up': KeyInfo(), + 'only-one': KeyInfo(), + '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(), + 'use-encryption': KeyInfo(), + 'use-ipv6': KeyInfo(), + 'use-mpls': KeyInfo(), + 'use-upnp': KeyInfo(), + 'wins-server': KeyInfo(can_disable=True), + }, + ), + ('queue', 'type'): APIData( + has_identifier=True, + fields={ + 'kind': KeyInfo(), + 'mq-pfifo-limit': KeyInfo(), + 'name': KeyInfo(), + 'pcq-burst-rate': KeyInfo(), + 'pcq-burst-threshold': KeyInfo(), + 'pcq-burst-time': KeyInfo(), + 'pcq-classifier': KeyInfo(), + 'pcq-dst-address-mask': KeyInfo(), + 'pcq-dst-address6-mask': KeyInfo(), + 'pcq-limit': KeyInfo(), + 'pcq-rate': KeyInfo(), + 'pcq-src-address-mask': KeyInfo(), + 'pcq-src-address6-mask': KeyInfo(), + 'pcq-total-limit': KeyInfo(), + 'pfifo-limit': KeyInfo(), + 'red-avg-packet': KeyInfo(), + 'red-burst': KeyInfo(), + 'red-limit': KeyInfo(), + 'red-max-threshold': KeyInfo(), + 'red-min-threshold': KeyInfo(), + 'sfq-allot': KeyInfo(), + 'sfq-perturb': KeyInfo(), + }, + ), + ('routing', 'bgp', 'instance'): APIData( + 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(), + '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(), + }, + ), + ('system', 'logging', 'action'): APIData( + fully_understood=True, + primary_keys=('name',), + 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( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'name': KeyInfo(), + 'policy': KeyInfo(), + 'skin': KeyInfo(default='default'), + }, + ), + ('caps-man', 'manager'): APIData( + 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( + 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( + primary_keys=('name', ), + fully_understood=True, + fields={ + 'comment': KeyInfo(can_disable=True, remove_value=''), + 'name': KeyInfo(), + 'regexp': KeyInfo(), + }, + ), + ('ip', 'hotspot', 'service-port'): APIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + 'ports': KeyInfo(), + }, + ), + ('ip', 'ipsec', 'policy'): APIData( + 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'), + 'template': KeyInfo(can_disable=True, remove_value=False), + # the tepmlate field can't really be changed once the item is created. This config captures the behavior best as it can + # i.e. tepmplate=yes is shown, tepmlate=no is hidden + 'tunnel': KeyInfo(default=False), + }, + ), + ('ip', 'service'): APIData( + fixed_entries=True, + primary_keys=('name', ), + fully_understood=True, + fields={ + 'address': KeyInfo(), + 'certificate': KeyInfo(), + 'disabled': KeyInfo(default=False), + 'name': KeyInfo(), + 'port': KeyInfo(), + 'tls-version': KeyInfo(), + }, + ), + ('system', 'logging'): APIData( + fully_understood=True, + fields={ + 'action': KeyInfo(default='memory'), + 'disabled': KeyInfo(default=False), + 'prefix': KeyInfo(default=''), + 'topics': KeyInfo(default=''), + }, + ), + ('system', 'resource', 'irq'): APIData( + has_identifier=True, + fields={ + 'cpu': KeyInfo(), + }, + ), + ('system', 'scheduler'): APIData( + 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( + 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/ansible_collections/community/routeros/plugins/module_utils/_version.py b/ansible_collections/community/routeros/plugins/module_utils/_version.py new file mode 100644 index 000000000..f7954074e --- /dev/null +++ b/ansible_collections/community/routeros/plugins/module_utils/_version.py @@ -0,0 +1,345 @@ +# Vendored copy of distutils/version.py from CPython 3.9.5 +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# Copyright (c) 2001-2022 Python Software Foundation. All rights reserved. +# PSF License (see LICENSES/PSF-2.0.txt or https://opensource.org/licenses/Python-2.0) +# SPDX-License-Identifier: PSF-2.0 +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. + +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + +try: + RE_FLAGS = re.VERBOSE | re.ASCII +except AttributeError: + RE_FLAGS = re.VERBOSE + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__(self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# _cmp (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion(Version): + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + + The following are examples of invalid version numbers: + + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', + RE_FLAGS) + + def parse(self, vstring): + match = self.version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + (major, minor, patch, prerelease, prerelease_num) = \ + match.group(1, 2, 4, 5, 6) + + if patch: + self.version = tuple(map(int, [major, minor, patch])) + else: + self.version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + self.prerelease = (prerelease[0], int(prerelease_num)) + else: + self.prerelease = None + + def __str__(self): + if self.version[2] == 0: + vstring = '.'.join(map(str, self.version[0:2])) + else: + vstring = '.'.join(map(str, self.version)) + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) + + return vstring + + def _cmp(self, other): + if isinstance(other, str): + other = StrictVersion(other) + elif not isinstance(other, StrictVersion): + return NotImplemented + + if self.version != other.version: + # numeric versions don't match + # prerelease stuff doesn't matter + if self.version < other.version: + return -1 + else: + return 1 + + # have to compare prerelease + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if (not self.prerelease and not other.prerelease): + return 0 + elif (self.prerelease and not other.prerelease): + return -1 + elif (not self.prerelease and other.prerelease): + return 1 + elif (self.prerelease and other.prerelease): + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 + else: + raise AssertionError("never get here") + +# end class StrictVersion + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + + +class LooseVersion(Version): + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def parse(self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) if x and x != '.'] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + def __str__(self): + return self.vstring + + def __repr__(self): + return "LooseVersion ('%s')" % str(self) + + def _cmp(self, other): + if isinstance(other, str): + other = LooseVersion(other) + elif not isinstance(other, LooseVersion): + return NotImplemented + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + +# end class LooseVersion diff --git a/ansible_collections/community/routeros/plugins/module_utils/api.py b/ansible_collections/community/routeros/plugins/module_utils/api.py new file mode 100644 index 000000000..5c598f3eb --- /dev/null +++ b/ansible_collections/community/routeros/plugins/module_utils/api.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de> +# Copyright (c) 2020, Nikolay Dachev <nikolay@dachev.info> +# 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 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) + 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): + 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'], + ) diff --git a/ansible_collections/community/routeros/plugins/module_utils/quoting.py b/ansible_collections/community/routeros/plugins/module_utils/quoting.py new file mode 100644 index 000000000..4b7098971 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/module_utils/quoting.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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/ansible_collections/community/routeros/plugins/module_utils/routeros.py b/ansible_collections/community/routeros/plugins/module_utils/routeros.py new file mode 100644 index 000000000..c2bd09c75 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/module_utils/routeros.py @@ -0,0 +1,153 @@ +# 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 + +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 = {} + +routeros_provider_spec = { + 'host': dict(), + 'port': dict(type='int'), + 'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + 'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True), + 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + 'timeout': dict(type='int') +} +routeros_argument_spec = {} + + +def get_provider_argspec(): + return routeros_provider_spec + + +def get_connection(module): + if hasattr(module, '_routeros_connection'): + return module._routeros_connection + + capabilities = get_capabilities(module) + network_api = capabilities.get('network_api') + if network_api == 'cliconf': + module._routeros_connection = Connection(module._socket_path) + else: + module.fail_json(msg='Invalid connection type %s' % network_api) + + return module._routeros_connection + + +def get_capabilities(module): + if hasattr(module, '_routeros_capabilities'): + return module._routeros_capabilities + + try: + capabilities = Connection(module._socket_path).get_capabilities() + module._routeros_capabilities = json.loads(capabilities) + return module._routeros_capabilities + except ConnectionError as exc: + module.fail_json(msg=to_native(exc, errors='surrogate_then_replace')) + + +def get_defaults_flag(module): + connection = get_connection(module) + + try: + out = connection.get('/system default-configuration print') + except ConnectionError as exc: + module.fail_json(msg=to_native(exc, errors='surrogate_then_replace')) + + out = to_native(out, errors='surrogate_then_replace') + + commands = set() + for line in out.splitlines(): + if line.strip(): + commands.add(line.strip().split()[0]) + + if 'all' in commands: + return ['all'] + else: + return ['full'] + + +def get_config(module, flags=None): + flag_str = ' '.join(to_list(flags)) + + try: + return _DEVICE_CONFIGS[flag_str] + except KeyError: + connection = get_connection(module) + + try: + out = connection.get_config(flags=flags) + except ConnectionError as exc: + module.fail_json(msg=to_native(exc, errors='surrogate_then_replace')) + + cfg = to_native(out, errors='surrogate_then_replace').strip() + _DEVICE_CONFIGS[flag_str] = cfg + return cfg + + +def to_commands(module, commands): + spec = { + 'command': dict(key=True), + 'prompt': dict(), + 'answer': dict() + } + transform = ComplexList(spec, module) + 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) + + for cmd in to_list(commands): + if isinstance(cmd, dict): + command = cmd['command'] + prompt = cmd['prompt'] + answer = cmd['answer'] + else: + command = cmd + prompt = None + answer = None + + if should_add_leading_space(module): + command = " " + command + + try: + out = connection.get(command, prompt, answer) + except ConnectionError as exc: + module.fail_json(msg=to_native(exc, errors='surrogate_then_replace')) + + try: + out = to_native(out, errors='surrogate_or_strict') + except UnicodeError: + module.fail_json( + msg=u'Failed to decode output from %s: %s' % (cmd, to_native(out))) + + responses.append(out) + + return responses + + +def load_config(module, commands): + connection = get_connection(module) + + out = connection.edit_config(commands) diff --git a/ansible_collections/community/routeros/plugins/module_utils/version.py b/ansible_collections/community/routeros/plugins/module_utils/version.py new file mode 100644 index 000000000..dc01ffe8f --- /dev/null +++ b/ansible_collections/community/routeros/plugins/module_utils/version.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein <felix@fontein.de> +# 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 + + +# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can +# remove the _version.py file, and replace the following import by +# +# from ansible.module_utils.compat.version import LooseVersion + +from ._version import LooseVersion # noqa: F401, pylint: disable=unused-import diff --git a/ansible_collections/community/routeros/plugins/modules/api.py b/ansible_collections/community/routeros/plugins/modules/api.py new file mode 100644 index 000000000..f9c619fc1 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/modules/api.py @@ -0,0 +1,577 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020, Nikolay Dachev <nikolay@dachev.info> +# 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 = ''' +--- +module: api +author: "Nikolay Dachev (@NikolayDachev)" +short_description: Ansible module for RouterOS API +description: + - Ansible module for RouterOS API with the Python C(librouteros) library. + - This module can add, remove, update, query and execute arbitrary command in RouterOS via API. +notes: + - I(add), I(remove), I(update), I(cmd) and I(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 +options: + 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). + 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). + - 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. + - 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). + - 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. + - WHERE is key word which extend query. WHERE format is key operator value - with spaces. + - WHERE valid operators are C(==) or C(eq), C(!=) or C(not), C(>) or C(more), C(<) or C(less). + - 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. + - 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 I(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 I(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 I(attributes). + - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified. + type: str + is: + description: + - The operator to use for matching. + - For equality use C(==) or C(eq). For less use C(<) or C(less). For more use C(>) or C(more). + - Use C(in) to check whether the value is part of a list. In that case, I(value) must be a list. + - Either I(or) or all of I(attribute), I(is), and I(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 I(is=in). + - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified. + type: raw + or: + description: + - A list of conditions so that at least one of them has to match. + - Either I(or) or all of I(attribute), I(is), and I(value) have to be specified. + type: list + elements: dict + suboptions: + attribute: + description: + - The attribute to match. Must be part of I(attributes). + type: str + required: true + is: + description: + - The operator to use for matching. + - For equality use C(==) or C(eq). For less use C(<) or C(less). For more use C(>) or C(more). + - Use C(in) to check whether the value is part of a list. In that case, I(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 I(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). + type: str +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 = ''' +- name: Get example - ip address print + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "ip address" + register: ipaddrd_printout + +- name: Dump "Get example" output + ansible.builtin.debug: + msg: '{{ ipaddrd_printout }}' + +- 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: 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: Dump "Query example" output + ansible.builtin.debug: + msg: '{{ queryout }}' + +- 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: Dump "Extended query example" output + ansible.builtin.debug: + msg: '{{ extended_queryout }}' + +- name: Update example - ether2 ip addres 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: 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: Arbitrary command example "/system identity print" + community.routeros.api: + hostname: "{{ hostname }}" + password: "{{ password }}" + username: "{{ username }}" + path: "system identity" + cmd: "print" + register: arbitraryout + +- name: Dump "Arbitrary command example" output + ansible.builtin.debug: + msg: '{{ arbitraryout }}' +''' + +RETURN = ''' +--- +message: + 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.common.text.converters import to_native + +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 + +try: + from librouteros.exceptions import LibRouterosError + from librouteros.query import Key, Or +except Exception: + # Handled in api module_utils + pass + + +class ROS_api_module: + def __init__(self): + module_args = dict( + 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'), + 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', 'extended_query'),),) + + check_has_library(self.module) + + self.api = create_api(self.module) + + 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'] + self.arbitrary = self.module.params['cmd'] + + self.where = None + self.query = self.module.params['query'] + self.extended_query = self.module.params['extended_query'] + + self.result = dict( + message=[]) + + # create api base path + self.api_path = self.api_add_path(self.api, self.path) + + # 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 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 '<attribute> <operator> <value>' + 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): + 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() + for p in path: + api_path = api_path.join(p) + return api_path + + def api_get_all(self): + try: + for i in self.api_path: + self.result['message'].append(i) + self.return_result(False, True) + except LibRouterosError as e: + self.errors(e) + + def api_add(self): + param = self.list_to_dic(self.split_params(self.add)) + try: + self.result['message'].append("added: .id= %s" + % self.api_path.add(**param)) + self.return_result(True) + except LibRouterosError as e: + self.errors(e) + + def api_remove(self): + try: + self.api_path.remove(self.remove) + self.result['message'].append("removed: .id= %s" % self.remove) + self.return_result(True) + except LibRouterosError as e: + self.errors(e) + + def api_update(self): + param = self.list_to_dic(self.split_params(self.update)) + if '.id' not in param.keys(): + self.errors("missing '.id' for %s" % param) + try: + self.api_path.update(**param) + self.result['message'].append("updated: %s" % param) + self.return_result(True) + except LibRouterosError as e: + self.errors(e) + + def api_query(self): + keys = {} + for k in self.query: + if 'id' in k and k != ".id": + self.errors("'%s' must be '.id'" % k) + keys[k] = Key(k) + try: + if self.where: + if self.where[1] in ('==', 'eq'): + select = self.api_path.select(*keys).where(keys[self.where[0]] == self.where[2]) + elif self.where[1] in ('!=', 'not'): + select = self.api_path.select(*keys).where(keys[self.where[0]] != self.where[2]) + elif self.where[1] in ('>', 'more'): + select = self.api_path.select(*keys).where(keys[self.where[0]] > self.where[2]) + 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]) + else: + 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)) + if self.where: + msg = msg + ' WHERE %s' % ' '.join(self.where) + self.result['message'].append(msg) + self.return_result(False) + 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.split_params(self.arbitrary) + arb_cmd = self.arbitrary[0] + if len(self.arbitrary) > 1: + param = self.list_to_dic(self.arbitrary[1:]) + try: + arbitrary_result = self.api_path(arb_cmd, **param) + for i in arbitrary_result: + self.result['message'].append(i) + self.return_result(False) + except LibRouterosError as e: + self.errors(e) + + def return_result(self, ch_status=False, status=True): + if not status: + self.module.fail_json(msg=self.result['message']) + else: + self.module.exit_json(changed=ch_status, + msg=self.result['message']) + + def errors(self, e): + if e.__class__.__name__ == 'TrapError': + self.result['message'].append("%s" % e) + self.return_result(False, False) + self.result['message'].append("%s" % e) + self.return_result(False, False) + + +def main(): + + ROS_api_module() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/routeros/plugins/modules/api_facts.py b/ansible_collections/community/routeros/plugins/modules/api_facts.py new file mode 100644 index 000000000..f29723667 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/modules/api_facts.py @@ -0,0 +1,495 @@ +#!/usr/bin/python + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# Copyright (c) 2020, Nikolay Dachev <nikolay@dachev.info> +# 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 = ''' +--- +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_<fact>). 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 +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(interfaces), and C(routing). + - 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. + 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 = """ +- 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 = """ +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: I(gather_subset) contains C(default) + type: str + ansible_net_serialnum: + description: The serial number of the remote device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_version: + description: The operating system version running on the remote device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_hostname: + description: The configured hostname of the device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_arch: + description: The CPU architecture of the device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_uptime: + description: The uptime of the device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_cpu_load: + description: Current CPU load. + returned: I(gather_subset) contains C(default) + type: str + + # hardware + ansible_net_spacefree_mb: + description: The available disk space on the remote device in MiB. + returned: I(gather_subset) contains C(hardware) + type: dict + ansible_net_spacetotal_mb: + description: The total disk space on the remote device in MiB. + returned: I(gather_subset) contains C(hardware) + type: dict + ansible_net_memfree_mb: + description: The available free memory on the remote device in MiB. + returned: I(gather_subset) contains C(hardware) + type: int + ansible_net_memtotal_mb: + description: The total memory on the remote device in MiB. + returned: I(gather_subset) contains C(hardware) + type: int + + # interfaces + ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device. + returned: I(gather_subset) contains C(interfaces) + type: list + ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device. + returned: I(gather_subset) contains C(interfaces) + type: list + ansible_net_interfaces: + description: A hash of all interfaces running on the system. + returned: I(gather_subset) contains C(interfaces) + type: dict + ansible_net_neighbors: + description: The list of neighbors from the remote device. + returned: I(gather_subset) contains C(interfaces) + type: dict + + # routing + ansible_net_bgp_peer: + description: A dictionary with BGP peer information. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_bgp_vpnv4_route: + description: A dictionary with BGP vpnv4 route information. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_bgp_instance: + description: A dictionary with BGP instance information. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_route: + description: A dictionary for routes in all routing tables. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_ospf_instance: + description: A dictionary with OSPF instances. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_ospf_neighbor: + description: A dictionary with OSPF neighbors. + returned: I(gather_subset) contains C(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'] + if family not in self.facts['interfaces'][key]: + self.facts['interfaces'][key][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) + self.facts['interfaces'][key][family].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()) + +warnings = [] + + +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, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/routeros/plugins/modules/api_find_and_modify.py b/ansible_collections/community/routeros/plugins/modules/api_find_and_modify.py new file mode 100644 index 000000000..0be3f7039 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/modules/api_find_and_modify.py @@ -0,0 +1,327 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# 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 = ''' +--- +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 I(require_matches_min=N) together with I(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 +options: + path: + description: + - Path to query. + - An example value is C(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 I(path) that match all fields provided here. + - Use YAML C(~), or prepend keys with C(!), 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 I(find), set the keys of this option to the values specified here. + - Use YAML C(~), or prepend keys with C(!), 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 I(require_matches_min) is 0 or larger. + type: bool +seealso: + - module: community.routeros.api + - module: community.routeros.api_facts + - module: community.routeros.api_modify + - module: community.routeros.api_info +''' + +EXAMPLES = ''' +--- +- 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 + +- 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 +''' + +RETURN = ''' +--- +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 I(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 + + +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'), + ) + 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)) + + check_has_library(module) + api = create_api(module) + + path = split_path(module.params['path']) + + api_path = compose_api_path(api, path) + + old_data = list(api_path) + 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 = list(api_path) + + # 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/ansible_collections/community/routeros/plugins/modules/api_info.py b/ansible_collections/community/routeros/plugins/modules/api_info.py new file mode 100644 index 000000000..50228c063 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/modules/api_info.py @@ -0,0 +1,366 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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 = ''' +--- +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 I(handle_disabled) and + I(hide_defaults) options to control normalization, the I(include_dynamic) and I(include_builtin) options to also return + dynamic resp. builtin entries, and use I(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.attributes + - community.routeros.attributes.actiongroup_api + - community.routeros.attributes.info_module +attributes: + platform: + support: full + platforms: RouterOS +options: + path: + description: + - Path to query. + - An example value is C(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 configuration + - caps-man datapath + - caps-man manager + - caps-man provisioning + - caps-man security + - certificate settings + - 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 gre + - interface gre6 + - interface l2tp-server server + - interface list + - interface list member + - interface ovpn-server server + - interface pppoe-client + - interface pptp-server server + - interface sstp-server server + - interface vlan + - interface vrrp + - interface wireguard + - interface wireguard peers + - interface wireless align + - interface wireless cap + - interface wireless sniffer + - interface wireless snooper + - ip accounting + - ip accounting web-access + - ip address + - ip arp + - ip cloud + - ip cloud advanced + - ip dhcp-client + - ip dhcp-client option + - ip dhcp-server + - ip dhcp-server config + - ip dhcp-server lease + - ip dhcp-server network + - ip dns + - 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 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 vrf + - ip service + - ip settings + - ip smb + - ip socks + - ip ssh + - ip tftp settings + - ip traffic-flow + - ip traffic-flow ipfix + - ip upnp + - ipv6 address + - ipv6 dhcp-client + - ipv6 dhcp-server + - ipv6 dhcp-server option + - ipv6 firewall address-list + - ipv6 firewall filter + - ipv6 firewall mangle + - ipv6 firewall raw + - ipv6 nd + - ipv6 nd prefix default + - ipv6 route + - ipv6 settings + - mpls + - mpls ldp + - port firmware + - ppp aaa + - queue interface + - queue tree + - radius incoming + - routing bgp instance + - routing mme + - routing ospf area + - routing ospf area range + - routing ospf instance + - routing ospf interface-template + - routing pimsm instance + - routing pimsm interface-template + - routing rip + - routing ripng + - snmp + - snmp community + - system clock + - system clock manual + - 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 routerboard settings + - system scheduler + - system script + - system upgrade mirror + - system ups + - system watchdog + - tool bandwidth-server + - tool e-mail + - tool graphing + - tool mac-server + - tool mac-server mac-winbox + - tool mac-server ping + - tool romon + - tool sms + - tool sniffer + - tool traffic-generator + - user aaa + - user group + # 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. + - C(exclamation) prepends the keys with C(!) in the output with value C(null). + - C(null-value) uses the regular key with value C(null). + - C(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 C(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 C(true), they are returned as well, and the C(builtin) keys are returned as well. + type: bool + default: false + version_added: 2.4.0 +seealso: + - module: community.routeros.api + - module: community.routeros.api_facts + - module: community.routeros.api_find_and_modify + - module: community.routeros.api_modify +''' + +EXAMPLES = ''' +--- +- 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 = ''' +--- +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, +) + +from ansible_collections.community.routeros.plugins.module_utils._api_data import ( + PATHS, + join_path, + 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 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), + ) + module_args.update(api_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']) + path_info = PATHS.get(tuple(path)) + if path_info is None: + module.fail_json(msg='Path /{path} is not yet supported'.format(path='/'.join(path))) + + handle_disabled = module.params['handle_disabled'] + hide_defaults = module.params['hide_defaults'] + include_dynamic = module.params['include_dynamic'] + include_builtin = module.params['include_builtin'] + 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 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 in path_info.fields: + 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 + 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/ansible_collections/community/routeros/plugins/modules/api_modify.py b/ansible_collections/community/routeros/plugins/modules/api_modify.py new file mode 100644 index 000000000..5d410e9fb --- /dev/null +++ b/ansible_collections/community/routeros/plugins/modules/api_modify.py @@ -0,0 +1,1030 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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 = ''' +--- +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/). +requirements: + - Needs L(ordereddict,https://pypi.org/project/ordereddict) for Python 2.6 +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 +options: + path: + description: + - Path to query. + - An example value is C(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 configuration + - caps-man datapath + - caps-man manager + - caps-man provisioning + - caps-man security + - certificate settings + - 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 gre + - interface gre6 + - interface l2tp-server server + - interface list + - interface list member + - interface ovpn-server server + - interface pppoe-client + - interface pptp-server server + - interface sstp-server server + - interface vlan + - interface vrrp + - interface wireguard + - interface wireguard peers + - interface wireless align + - interface wireless cap + - interface wireless sniffer + - interface wireless snooper + - ip accounting + - ip accounting web-access + - ip address + - ip arp + - ip cloud + - ip cloud advanced + - ip dhcp-client + - ip dhcp-client option + - ip dhcp-server + - ip dhcp-server config + - ip dhcp-server lease + - ip dhcp-server network + - ip dns + - 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 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 vrf + - ip service + - ip settings + - ip smb + - ip socks + - ip ssh + - ip tftp settings + - ip traffic-flow + - ip traffic-flow ipfix + - ip upnp + - ipv6 address + - ipv6 dhcp-client + - ipv6 dhcp-server + - ipv6 dhcp-server option + - ipv6 firewall address-list + - ipv6 firewall filter + - ipv6 firewall mangle + - ipv6 firewall raw + - ipv6 nd + - ipv6 nd prefix default + - ipv6 route + - ipv6 settings + - mpls + - mpls ldp + - port firmware + - ppp aaa + - queue interface + - queue tree + - radius incoming + - routing bgp instance + - routing mme + - routing ospf area + - routing ospf area range + - routing ospf instance + - routing ospf interface-template + - routing pimsm instance + - routing pimsm interface-template + - routing rip + - routing ripng + - snmp + - snmp community + - system clock + - system clock manual + - 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 routerboard settings + - system scheduler + - system script + - system upgrade mirror + - system ups + - system watchdog + - tool bandwidth-server + - tool e-mail + - tool graphing + - tool mac-server + - tool mac-server mac-winbox + - tool mac-server ping + - tool romon + - tool sms + - tool sniffer + - tool traffic-generator + - user aaa + - user group + # 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 I(data). + - Requires I(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 I(data). + - C(ignore) ignores them. + - C(remove) removes them. + type: str + choices: + - ignore + - remove + default: ignore + handle_entries_content: + description: + - For a single entry in I(data), this describes how to handle fields that are not mentioned + in that entry, but appear in the actual config. + - If C(ignore), they are not modified. + - If C(remove), they are removed. If at least one cannot be removed, the module will fail. + - If C(remove_as_much_as_possible), all that can be removed will be removed. The ones that + cannot be removed will be kept. + type: str + choices: + - ignore + - remove + - remove_as_much_as_possible + default: ignore +seealso: + - module: community.routeros.api + - module: community.routeros.api_facts + - module: community.routeros.api_find_and_modify + - module: community.routeros.api_info +''' + +EXAMPLES = ''' +--- +- 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': +''' + +RETURN = ''' +--- +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, +) + +from ansible_collections.community.routeros.plugins.module_utils._api_data import ( + PATHS, + join_path, + split_path, +) + +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 + 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 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') + 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)) + 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 sync_list(module, api, path, path_info): + 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('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): + 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) + 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: + 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)) + + # 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): + 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): + 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_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 + ]), + )) + 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)) + + # 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): + 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 main(): + path_choices = sorted([join_path(path) for path, path_info in PATHS.items() if get_backend(path_info) is not None]) + 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), + ) + module_args.update(api_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']) + path_info = PATHS.get(tuple(path)) + 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))) + + backend(module, api, path, path_info) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/routeros/plugins/modules/command.py b/ansible_collections/community/routeros/plugins/modules/command.py new file mode 100644 index 000000000..84426025c --- /dev/null +++ b/ansible_collections/community/routeros/plugins/modules/command.py @@ -0,0 +1,210 @@ +#!/usr/bin/python + +# 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 = ''' +--- +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. + - 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. +notes: + - The module declares that it B(supports check mode). This is a bug and will + be changed in community.routeros 3.0.0. +extends_documentation_fragment: + - community.routeros.attributes +attributes: + check_mode: + support: partial + details: + - The module claims to support check mode, but it simply always executes the command. + diff_mode: + support: none + platform: + support: full + platforms: RouterOS +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. + 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. + 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. + 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. + 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. + 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 = """ +- name: Run command on remote devices + community.routeros.command: + commands: /system routerboard print + +- name: Run command and check to see if output contains routeros + community.routeros.command: + commands: /system resource print + wait_for: result[0] contains MikroTik + +- name: Run multiple commands on remote nodes + community.routeros.command: + commands: + - /system routerboard print + - /system identity print + +- name: Run multiple commands and evaluate the output + community.routeros.command: + commands: + - /system routerboard print + - /interface ethernet print + wait_for: + - result[0] contains x86 + - result[1] contains ether1 +""" + +RETURN = """ +stdout: + 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 + returned: always apart from low level errors (such as action plugin) + type: list + sample: [['...', '...'], ['...'], ['...']] +failed_conditions: + description: The list of conditionals that have failed + returned: failed + type: list + sample: ['...', '...'] +""" + +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.parsing import Conditional +from ansible.module_utils.six import string_types + + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + + +def main(): + """main entry point for module execution + """ + argument_spec = dict( + commands=dict(type='list', elements='str', required=True), + + 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') + ) + + argument_spec.update(routeros_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + result = {'changed': False} + + wait_for = module.params['wait_for'] or list() + conditionals = [Conditional(c) for c in wait_for] + + retries = module.params['retries'] + interval = module.params['interval'] + match = module.params['match'] + + while retries > 0: + responses = run_commands(module, module.params['commands']) + + for item in list(conditionals): + if item(responses): + if match == 'any': + conditionals = list() + break + conditionals.remove(item) + + if not conditionals: + break + + time.sleep(interval) + retries -= 1 + + if conditionals: + failed_conditions = [item.raw for item in conditionals] + msg = 'One or more conditional statements have not been satisfied' + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + result.update({ + 'changed': True, + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/routeros/plugins/modules/facts.py b/ansible_collections/community/routeros/plugins/modules/facts.py new file mode 100644 index 000000000..85c0b37c4 --- /dev/null +++ b/ansible_collections/community/routeros/plugins/modules/facts.py @@ -0,0 +1,663 @@ +#!/usr/bin/python + +# 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 = ''' +--- +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 RouterOS. This module prepends all of the + base network fact keys with C(ansible_net_<fact>). 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 +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), C(interfaces), and C(routing). + - 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. + required: false + 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 = """ +- name: Collect all facts from the device + community.routeros.facts: + gather_subset: all + +- name: Collect only the config and default facts + community.routeros.facts: + gather_subset: + - config + +- name: Do not collect hardware facts + community.routeros.facts: + gather_subset: + - "!hardware" +""" + +RETURN = """ +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: I(gather_subset) contains C(default) + type: str + ansible_net_serialnum: + description: The serial number of the remote device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_version: + description: The operating system version running on the remote device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_hostname: + description: The configured hostname of the device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_arch: + description: The CPU architecture of the device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_uptime: + description: The uptime of the device. + returned: I(gather_subset) contains C(default) + type: str + ansible_net_cpu_load: + description: Current CPU load. + returned: I(gather_subset) contains C(default) + type: str + + # hardware + ansible_net_spacefree_mb: + description: The available disk space on the remote device in MiB. + returned: I(gather_subset) contains C(hardware) + type: dict + ansible_net_spacetotal_mb: + description: The total disk space on the remote device in MiB. + returned: I(gather_subset) contains C(hardware) + type: dict + ansible_net_memfree_mb: + description: The available free memory on the remote device in MiB. + returned: I(gather_subset) contains C(hardware) + type: int + ansible_net_memtotal_mb: + description: The total memory on the remote device in MiB. + returned: I(gather_subset) contains C(hardware) + type: int + + # config + ansible_net_config: + description: The current active config from the device. + returned: I(gather_subset) contains C(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: I(gather_subset) contains C(config) + type: str + version_added: 1.2.0 + + # interfaces + ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device. + returned: I(gather_subset) contains C(interfaces) + type: list + ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device. + returned: I(gather_subset) contains C(interfaces) + type: list + ansible_net_interfaces: + description: A hash of all interfaces running on the system. + returned: I(gather_subset) contains C(interfaces) + type: dict + ansible_net_neighbors: + description: The list of neighbors from the remote device. + returned: I(gather_subset) contains C(interfaces) + type: dict + + # routing + ansible_net_bgp_peer: + description: A dictionary with BGP peer information. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_bgp_vpnv4_route: + description: A dictionary with BGP vpnv4 route information. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_bgp_instance: + description: A dictionary with BGP instance information. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_route: + description: A dictionary for routes in all routing tables. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_ospf_instance: + description: A dictionary with OSPF instances. + returned: I(gather_subset) contains C(routing) + type: dict + ansible_net_ospf_neighbor: + description: A dictionary with OSPF neighbors. + returned: I(gather_subset) contains C(routing) + type: dict +""" +import re + +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.module_utils.six import iteritems + + +class FactsBase(object): + + COMMANDS = list() + + def __init__(self, module): + self.module = module + self.facts = dict() + self.responses = None + + def populate(self): + self.responses = run_commands(self.module, commands=self.COMMANDS, check_rc=False) + + def run(self, cmd): + return run_commands(self.module, commands=cmd, check_rc=False) + + +class Default(FactsBase): + + COMMANDS = [ + '/system identity print without-paging', + '/system resource print without-paging', + '/system routerboard print without-paging' + ] + + def populate(self): + super(Default, self).populate() + data = self.responses[0] + if data: + self.facts['hostname'] = self.parse_hostname(data) + data = self.responses[1] + if data: + self.facts['version'] = self.parse_version(data) + self.facts['arch'] = self.parse_arch(data) + self.facts['uptime'] = self.parse_uptime(data) + self.facts['cpu_load'] = self.parse_cpu_load(data) + data = self.responses[2] + if data: + self.facts['model'] = self.parse_model(data) + self.facts['serialnum'] = self.parse_serialnum(data) + + def parse_hostname(self, data): + match = re.search(r'name:\s(.*)\s*$', data, re.M) + if match: + return match.group(1) + + def parse_version(self, data): + match = re.search(r'version:\s(.*)\s*$', data, re.M) + if match: + return match.group(1) + + def parse_model(self, data): + match = re.search(r'model:\s(.*)\s*$', data, re.M) + if match: + return match.group(1) + + def parse_arch(self, data): + match = re.search(r'architecture-name:\s(.*)\s*$', data, re.M) + if match: + return match.group(1) + + def parse_uptime(self, data): + match = re.search(r'uptime:\s(.*)\s*$', data, re.M) + if match: + return match.group(1) + + def parse_cpu_load(self, data): + match = re.search(r'cpu-load:\s(.*)\s*$', data, re.M) + if match: + return match.group(1) + + def parse_serialnum(self, data): + match = re.search(r'serial-number:\s(.*)\s*$', data, re.M) + if match: + return match.group(1) + + +class Hardware(FactsBase): + + COMMANDS = [ + '/system resource print without-paging' + ] + + def populate(self): + super(Hardware, self).populate() + data = self.responses[0] + if data: + self.parse_filesystem_info(data) + self.parse_memory_info(data) + + def parse_filesystem_info(self, data): + match = re.search(r'free-hdd-space:\s(.*)([KMG]iB)', data, re.M) + if match: + self.facts['spacefree_mb'] = self.to_megabytes(match) + match = re.search(r'total-hdd-space:\s(.*)([KMG]iB)', data, re.M) + if match: + self.facts['spacetotal_mb'] = self.to_megabytes(match) + + def parse_memory_info(self, data): + match = re.search(r'free-memory:\s(\d+\.?\d*)([KMG]iB)', data, re.M) + if match: + self.facts['memfree_mb'] = self.to_megabytes(match) + match = re.search(r'total-memory:\s(\d+\.?\d*)([KMG]iB)', data, re.M) + if match: + self.facts['memtotal_mb'] = self.to_megabytes(match) + + def to_megabytes(self, data): + if data.group(2) == 'KiB': + return float(data.group(1)) / 1024 + elif data.group(2) == 'MiB': + return float(data.group(1)) + elif data.group(2) == 'GiB': + return float(data.group(1)) * 1024 + else: + return None + + +class Config(FactsBase): + + COMMANDS = [ + '/export verbose', + '/export', + ] + + RM_DATE_RE = re.compile(r'^# [a-z0-9/][a-z0-9/]* [0-9:]* by RouterOS') + + def populate(self): + super(Config, self).populate() + + data = self.responses[0] + if data: + self.facts['config'] = data + + data = self.responses[1] + if data: + # remove datetime + data = re.sub(self.RM_DATE_RE, r'# RouterOS', data) + self.facts['config_nonverbose'] = data + + +class Interfaces(FactsBase): + + COMMANDS = [ + '/interface print detail without-paging', + '/ip address print detail without-paging', + '/ipv6 address print detail without-paging', + '/ip neighbor print detail without-paging' + ] + + DETAIL_RE = re.compile(r'([\w\d\-]+)=\"?(\w{3}/\d{2}/\d{4}\s\d{2}:\d{2}:\d{2}|[\w\d\-\.:/]+)') + WRAPPED_LINE_RE = re.compile(r'^\s+(?!\d)') + + def populate(self): + super(Interfaces, self).populate() + + self.facts['interfaces'] = dict() + self.facts['all_ipv4_addresses'] = list() + self.facts['all_ipv6_addresses'] = list() + self.facts['neighbors'] = list() + + 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'] + if family not in self.facts['interfaces'][key]: + self.facts['interfaces'][key][family] = list() + addr, subnet = value['address'].split("/") + ip = dict(address=addr.strip(), subnet=subnet.strip()) + self.add_ip_address(addr.strip(), family) + self.facts['interfaces'][key][family].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 preprocess(self, data): + preprocessed = list() + for line in data.split('\n'): + if len(line) == 0 or line[:5] == 'Flags': + continue + elif not preprocessed or not re.match(self.WRAPPED_LINE_RE, line): + preprocessed.append(line) + else: + preprocessed[-1] += line + return preprocessed + + def parse_interfaces(self, data): + facts = dict() + data = self.preprocess(data) + for line in data: + parsed = dict(re.findall(self.DETAIL_RE, line)) + if "name" not in parsed: + continue + facts[parsed["name"]] = dict(re.findall(self.DETAIL_RE, line)) + return facts + + def parse_detail(self, data): + data = self.preprocess(data) + for line in data: + parsed = dict(re.findall(self.DETAIL_RE, line)) + if "interface" not in parsed: + continue + yield parsed + + +class Routing(FactsBase): + + COMMANDS = [ + '/routing bgp peer print detail without-paging', + '/routing bgp vpnv4-route print detail without-paging', + '/routing bgp instance print detail without-paging', + '/ip route print detail without-paging', + '/routing ospf instance print detail without-paging', + '/routing ospf neighbor print detail without-paging' + ] + + DETAIL_RE = re.compile(r'([\w\d\-]+)=\"?(\w{3}/\d{2}/\d{4}\s\d{2}:\d{2}:\d{2}|[\w\d\-\.:/]+)') + WRAPPED_LINE_RE = re.compile(r'^\s+(?!\d)') + + def populate(self): + super(Routing, self).populate() + self.facts['bgp_peer'] = dict() + self.facts['bgp_vpnv4_route'] = dict() + self.facts['bgp_instance'] = dict() + self.facts['route'] = dict() + self.facts['ospf_instance'] = dict() + self.facts['ospf_neighbor'] = dict() + data = self.responses[0] + if data: + peer = self.parse_bgp_peer(data) + self.populate_bgp_peer(peer) + data = self.responses[1] + if data: + vpnv4 = self.parse_vpnv4_route(data) + self.populate_vpnv4_route(vpnv4) + data = self.responses[2] + if data: + instance = self.parse_instance(data) + self.populate_bgp_instance(instance) + data = self.responses[3] + if data: + route = self.parse_route(data) + self.populate_route(route) + data = self.responses[4] + if data: + instance = self.parse_instance(data) + self.populate_ospf_instance(instance) + data = self.responses[5] + if data: + instance = self.parse_ospf_neighbor(data) + self.populate_ospf_neighbor(instance) + + def preprocess(self, data): + preprocessed = list() + for line in data.split('\n'): + if len(line) == 0 or line[:5] == 'Flags': + continue + elif not preprocessed or not re.match(self.WRAPPED_LINE_RE, line): + preprocessed.append(line) + else: + preprocessed[-1] += line + return preprocessed + + def parse_name(self, data): + match = re.search(r'name=.(\S+\b)', data, re.M) + if match: + return match.group(1) + + def parse_interface(self, data): + match = re.search(r'interface=([\w\d\-]+)', data, re.M) + if match: + return match.group(1) + + def parse_instance_name(self, data): + match = re.search(r'instance=([\w\d\-]+)', data, re.M) + if match: + return match.group(1) + + def parse_routing_mark(self, data): + match = re.search(r'routing-mark=([\w\d\-]+)', data, re.M) + if match: + return match.group(1) + else: + match = 'main' + return match + + def parse_bgp_peer(self, data): + facts = dict() + data = self.preprocess(data) + for line in data: + name = self.parse_name(line) + facts[name] = dict() + for (key, value) in re.findall(self.DETAIL_RE, line): + facts[name][key] = value + return facts + + def parse_instance(self, data): + facts = dict() + data = self.preprocess(data) + for line in data: + name = self.parse_name(line) + facts[name] = dict() + for (key, value) in re.findall(self.DETAIL_RE, line): + facts[name][key] = value + return facts + + def parse_vpnv4_route(self, data): + facts = dict() + data = self.preprocess(data) + for line in data: + name = self.parse_interface(line) + facts[name] = dict() + for (key, value) in re.findall(self.DETAIL_RE, line): + facts[name][key] = value + return facts + + def parse_route(self, data): + facts = dict() + data = self.preprocess(data) + for line in data: + name = self.parse_routing_mark(line) + facts[name] = dict() + for (key, value) in re.findall(self.DETAIL_RE, line): + facts[name][key] = value + return facts + + def parse_ospf_instance(self, data): + facts = dict() + data = self.preprocess(data) + for line in data: + name = self.parse_name(line) + facts[name] = dict() + for (key, value) in re.findall(self.DETAIL_RE, line): + facts[name][key] = value + return facts + + def parse_ospf_neighbor(self, data): + facts = dict() + data = self.preprocess(data) + for line in data: + name = self.parse_instance_name(line) + facts[name] = dict() + for (key, value) in re.findall(self.DETAIL_RE, line): + facts[name][key] = value + return facts + + def populate_bgp_peer(self, data): + for key, value in iteritems(data): + self.facts['bgp_peer'][key] = value + + def populate_vpnv4_route(self, data): + for key, value in iteritems(data): + self.facts['bgp_vpnv4_route'][key] = value + + def populate_bgp_instance(self, data): + for key, value in iteritems(data): + self.facts['bgp_instance'][key] = value + + def populate_route(self, data): + for key, value in iteritems(data): + self.facts['route'][key] = value + + def populate_ospf_instance(self, data): + for key, value in iteritems(data): + self.facts['ospf_instance'][key] = value + + def populate_ospf_neighbor(self, data): + for key, value in iteritems(data): + self.facts['ospf_neighbor'][key] = value + + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, + routing=Routing, +) + +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', elements='str') + ) + + argument_spec.update(routeros_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + 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 = dict() + facts['gather_subset'] = list(runable_subsets) + + instances = list() + for key in runable_subsets: + instances.append(FACT_SUBSETS[key](module)) + + for inst in instances: + inst.populate() + facts.update(inst.facts) + + ansible_facts = dict() + for key, value in iteritems(facts): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/routeros/plugins/terminal/routeros.py b/ansible_collections/community/routeros/plugins/terminal/routeros.py new file mode 100644 index 000000000..9d50fa25f --- /dev/null +++ b/ansible_collections/community/routeros/plugins/terminal/routeros.py @@ -0,0 +1,53 @@ +# 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 re + +from ansible.errors import AnsibleConnectionFailure +from ansible.plugins.terminal import TerminalBase +from ansible.utils.display import Display + +display = Display() + + +class TerminalModule(TerminalBase): + + ansi_re = [ + # check ECMA-48 Section 5.4 (Control Sequences) + re.compile(br'(\x1b\[\?1h\x1b=)'), + re.compile(br'((?:\x9b|\x1b\x5b)[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e])'), + re.compile(br'\x08.') + ] + + terminal_initial_prompt = [ + br'\x1bZ', + ] + + terminal_initial_answer = b'\x1b/Z' + + terminal_stdout_re = [ + re.compile(br"\x1b<"), + re.compile(br"\[[\w\-\.]+\@[\w\s\-\.\/]+\] ?(<SAFE)?> ?$"), + re.compile(br"Please press \"Enter\" to continue!"), + re.compile(br"Do you want to see the software license\? \[Y\/n\]: ?"), + ] + + terminal_stderr_re = [ + re.compile(br"\nbad command name"), + re.compile(br"\nno such item"), + re.compile(br"\ninvalid value for"), + ] + + def on_open_shell(self): + prompt = self._get_prompt() + try: + if prompt.strip().endswith(b':'): + self._exec_cli_command(b' ') + if prompt.strip().endswith(b'!'): + self._exec_cli_command(b'\n') + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to bypass license prompt') diff --git a/ansible_collections/community/routeros/tests/config.yml b/ansible_collections/community/routeros/tests/config.yml new file mode 100644 index 000000000..38590f2e4 --- /dev/null +++ b/ansible_collections/community/routeros/tests/config.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 + +# See template for more information: +# https://github.com/ansible/ansible/blob/devel/test/lib/ansible_test/config/config.yml +modules: + python_requires: default diff --git a/ansible_collections/community/routeros/tests/ee/all.yml b/ansible_collections/community/routeros/tests/ee/all.yml new file mode 100644 index 000000000..26f198b4f --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/aliases b/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/aliases new file mode 100644 index 000000000..ddba81818 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/tasks/main.yml b/ansible_collections/community/routeros/tests/ee/roles/filter_quoting/tasks/main.yml new file mode 100644 index 000000000..e7a2d29a1 --- /dev/null +++ b/ansible_collections/community/routeros/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: + - >- + result.msg == "Unexpected end of string during escaped parameter" + +- 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/ansible_collections/community/routeros/tests/ee/roles/smoke/tasks/main.yml b/ansible_collections/community/routeros/tests/ee/roles/smoke/tasks/main.yml new file mode 100644 index 000000000..b992c8e18 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/integration/requirements.yml b/ansible_collections/community/routeros/tests/integration/requirements.yml new file mode 100644 index 000000000..6a22736b5 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/aliases b/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/aliases new file mode 100644 index 000000000..ddba81818 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/tasks/main.yml b/ansible_collections/community/routeros/tests/integration/targets/filter_quoting/tasks/main.yml new file mode 100644 index 000000000..e7a2d29a1 --- /dev/null +++ b/ansible_collections/community/routeros/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: + - >- + result.msg == "Unexpected end of string during escaped parameter" + +- 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/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json new file mode 100644 index 000000000..9a28d174f --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json @@ -0,0 +1,13 @@ +{ + "include_symlinks": false, + "prefixes": [ + "docs/docsite/", + "plugins/", + "roles/" + ], + "output": "path-line-column-message", + "requirements": [ + "ansible-core", + "antsibull-docs" + ] +} diff --git a/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json.license b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.json.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/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.py b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.py new file mode 100755 index 000000000..c636beb08 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/extra-docs.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# 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 +"""Check extra collection docs with antsibull-docs.""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import sys +import subprocess + + +def main(): + """Main entry point.""" + env = os.environ.copy() + suffix = ':{env}'.format(env=env["ANSIBLE_COLLECTIONS_PATH"]) if 'ANSIBLE_COLLECTIONS_PATH' in env else '' + env['ANSIBLE_COLLECTIONS_PATH'] = '{root}{suffix}'.format(root=os.path.dirname(os.path.dirname(os.path.dirname(os.getcwd()))), suffix=suffix) + p = subprocess.run( + ['antsibull-docs', 'lint-collection-docs', '--plugin-docs', '--disallow-semantic-markup', '--skip-rstcheck', '.'], + env=env, + 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/ansible_collections/community/routeros/tests/sanity/extra/licenses.json b/ansible_collections/community/routeros/tests/sanity/extra/licenses.json new file mode 100644 index 000000000..50e47ca88 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/licenses.json @@ -0,0 +1,4 @@ +{ + "include_symlinks": false, + "output": "path-message" +} diff --git a/ansible_collections/community/routeros/tests/sanity/extra/licenses.json.license b/ansible_collections/community/routeros/tests/sanity/extra/licenses.json.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/licenses.json.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/ansible_collections/community/routeros/tests/sanity/extra/licenses.py b/ansible_collections/community/routeros/tests/sanity/extra/licenses.py new file mode 100755 index 000000000..80eb795ef --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/licenses.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# Copyright (c) 2022, Felix Fontein <felix@fontein.de> +# 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 +"""Prevent files without a correct license identifier from being added to the source tree.""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import glob +import sys + + +def format_license_list(licenses): + if not licenses: + return '(empty)' + return ', '.join(['"%s"' % license for license in licenses]) + + +def find_licenses(filename, relax=False): + spdx_license_identifiers = [] + other_license_identifiers = [] + has_copyright = False + try: + with open(filename, 'r', encoding='utf-8') as f: + for line in f: + line = line.rstrip() + if 'Copyright ' in line: + has_copyright = True + if 'Copyright: ' in line: + print('%s: found copyright line with "Copyright:". Please remove the colon.' % (filename, )) + if 'SPDX-FileCopyrightText: ' in line: + has_copyright = True + idx = line.find('SPDX-License-Identifier: ') + if idx >= 0: + lic_id = line[idx + len('SPDX-License-Identifier: '):] + spdx_license_identifiers.extend(lic_id.split(' OR ')) + if 'GNU General Public License' in line: + if 'v3.0+' in line: + other_license_identifiers.append('GPL-3.0-or-later') + if 'version 3 or later' in line: + other_license_identifiers.append('GPL-3.0-or-later') + if 'Simplified BSD License' in line: + other_license_identifiers.append('BSD-2-Clause') + if 'Apache License 2.0' in line: + other_license_identifiers.append('Apache-2.0') + if 'PSF License' in line or 'Python-2.0' in line: + other_license_identifiers.append('PSF-2.0') + if 'MIT License' in line: + other_license_identifiers.append('MIT') + except Exception as exc: + print('%s: error while processing file: %s' % (filename, exc)) + if len(set(spdx_license_identifiers)) < len(spdx_license_identifiers): + print('%s: found identical SPDX-License-Identifier values' % (filename, )) + if other_license_identifiers and set(other_license_identifiers) != set(spdx_license_identifiers): + print('%s: SPDX-License-Identifier yielded the license list %s, while manual guessing yielded the license list %s' % ( + filename, format_license_list(spdx_license_identifiers), format_license_list(other_license_identifiers))) + if not has_copyright and not relax: + print('%s: found no copyright notice' % (filename, )) + return sorted(spdx_license_identifiers) + + +def main(): + """Main entry point.""" + paths = sys.argv[1:] or sys.stdin.read().splitlines() + + # The following paths are allowed to have no license identifier + no_comments_allowed = [ + 'changelogs/fragments/*.yml', + 'changelogs/fragments/*.yaml', + ] + + # These files are completely ignored + ignore_paths = [ + '.ansible-test-timeout.json', + '.reuse/dep5', + 'LICENSES/*.txt', + 'COPYING', + ] + + no_comments_allowed = [fn for pattern in no_comments_allowed for fn in glob.glob(pattern)] + ignore_paths = [fn for pattern in ignore_paths for fn in glob.glob(pattern)] + + valid_licenses = [license_file[len('LICENSES/'):-len('.txt')] for license_file in glob.glob('LICENSES/*.txt')] + + for path in paths: + if path.startswith('./'): + path = path[2:] + if path in ignore_paths or path.startswith('tests/output/'): + continue + if os.stat(path).st_size == 0: + continue + if not path.endswith('.license') and os.path.exists(path + '.license'): + path = path + '.license' + valid_licenses_for_path = valid_licenses + if path.startswith('plugins/') and not path.startswith(('plugins/modules/', 'plugins/module_utils/')): + valid_licenses_for_path = [license for license in valid_licenses if license == 'GPL-3.0-or-later'] + licenses = find_licenses(path, relax=path in no_comments_allowed) + if not licenses: + if path not in no_comments_allowed: + print('%s: must have at least one license' % (path, )) + else: + for license in licenses: + if license not in valid_licenses_for_path: + print('%s: found not allowed license "%s", must be one of %s' % ( + path, license, format_license_list(valid_licenses_for_path))) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/routeros/tests/sanity/extra/licenses.py.license b/ansible_collections/community/routeros/tests/sanity/extra/licenses.py.license new file mode 100644 index 000000000..6c4958feb --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/licenses.py.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: 2022, Felix Fontein <felix@fontein.de> diff --git a/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json new file mode 100644 index 000000000..c789a7fd3 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json @@ -0,0 +1,7 @@ +{ + "include_symlinks": true, + "prefixes": [ + "plugins/" + ], + "output": "path-message" +} diff --git a/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json.license b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.json.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/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.py b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.py new file mode 100755 index 000000000..b39df83a1 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/no-unwanted-files.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# 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 +"""Prevent unwanted files from being added to the source tree.""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import os.path +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 = ( + ) + + yaml_directories = ( + 'plugins/test/', + 'plugins/filter/', + ) + + for path in paths: + if path in skip_paths: + continue + + if any(path.startswith(skip_directory) for skip_directory in skip_directories): + continue + + if os.path.islink(path): + print('%s: is a symbolic link' % (path, )) + elif not os.path.isfile(path): + print('%s: is not a regular file' % (path, )) + + ext = os.path.splitext(path)[1] + + if ext in ('.yml', ) and any(path.startswith(yaml_directory) for yaml_directory in yaml_directories): + continue + + 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/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json new file mode 100644 index 000000000..029699f0f --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json @@ -0,0 +1,8 @@ +{ + "include_symlinks": false, + "prefixes": [ + "docs/docsite/rst/api-guide.rst", + "plugins/modules/" + ], + "output": "path-line-column-message" +} diff --git a/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json.license b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.json.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/ansible_collections/community/routeros/tests/sanity/extra/update-docs.py b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.py new file mode 100644 index 000000000..68e2edf87 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/extra/update-docs.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# 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 +"""Check whether update-docs.py modifies something.""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import subprocess + + +def main(): + """Main entry point.""" + p = subprocess.run(['./update-docs.py'], check=False) + if p.returncode not in (0, 1): + print('{0}:0:0: unexpected return code {1}'.format(sys.argv[0], p.returncode)) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt new file mode 100644 index 000000000..876765a85 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt @@ -0,0 +1,6 @@ +update-docs.py compile-2.6 +update-docs.py compile-2.7 +update-docs.py compile-3.5 +update-docs.py future-import-boilerplate +update-docs.py metaclass-boilerplate +update-docs.py shebang diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.10.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt new file mode 100644 index 000000000..876765a85 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt @@ -0,0 +1,6 @@ +update-docs.py compile-2.6 +update-docs.py compile-2.7 +update-docs.py compile-3.5 +update-docs.py future-import-boilerplate +update-docs.py metaclass-boilerplate +update-docs.py shebang diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.11.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt new file mode 100644 index 000000000..ce635c32c --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt @@ -0,0 +1 @@ +update-docs.py shebang diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.12.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt new file mode 100644 index 000000000..ce635c32c --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt @@ -0,0 +1 @@ +update-docs.py shebang diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.13.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt new file mode 100644 index 000000000..ce635c32c --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt @@ -0,0 +1 @@ +update-docs.py shebang diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.14.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt new file mode 100644 index 000000000..ce635c32c --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt @@ -0,0 +1 @@ +update-docs.py shebang diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.15.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt new file mode 100644 index 000000000..ce635c32c --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt @@ -0,0 +1 @@ +update-docs.py shebang diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.16.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt b/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt new file mode 100644 index 000000000..876765a85 --- /dev/null +++ b/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt @@ -0,0 +1,6 @@ +update-docs.py compile-2.6 +update-docs.py compile-2.7 +update-docs.py compile-3.5 +update-docs.py future-import-boilerplate +update-docs.py metaclass-boilerplate +update-docs.py shebang diff --git a/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt.license b/ansible_collections/community/routeros/tests/sanity/ignore-2.9.txt.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/compat/__init__.py b/ansible_collections/community/routeros/tests/unit/compat/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/compat/__init__.py diff --git a/ansible_collections/community/routeros/tests/unit/compat/builtins.py b/ansible_collections/community/routeros/tests/unit/compat/builtins.py new file mode 100644 index 000000000..d548601d4 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/compat/builtins.py @@ -0,0 +1,20 @@ +# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# 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 + +# +# 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__ # noqa: F401, pylint: disable=unused-import +except ImportError: + BUILTINS = 'builtins' +else: + BUILTINS = '__builtin__' diff --git a/ansible_collections/community/routeros/tests/unit/compat/mock.py b/ansible_collections/community/routeros/tests/unit/compat/mock.py new file mode 100644 index 000000000..bdbea945e --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/compat/mock.py @@ -0,0 +1,109 @@ +# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# 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 + +''' +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 * # noqa: F401, pylint: disable=unused-import +except ImportError: + # Python 2 + # pylint: disable=wildcard-import,unused-wildcard-import + try: + from mock import * # noqa: F401, pylint: disable=unused-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/ansible_collections/community/routeros/tests/unit/compat/unittest.py b/ansible_collections/community/routeros/tests/unit/compat/unittest.py new file mode 100644 index 000000000..d50bab86f --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/compat/unittest.py @@ -0,0 +1,25 @@ +# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# 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 + +''' +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 * # noqa: F401, pylint: disable=unused-import + except ImportError: + print('You need unittest2 installed on python2.6.x to run tests') +else: + from unittest import * # noqa: F401, pylint: disable=unused-import diff --git a/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test__api_data.py b/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test__api_data.py new file mode 100644 index 000000000..1250fdaa5 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test__api_data.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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 ( + APIData, + KeyInfo, + split_path, + join_path, +) + + +def test_api_data_errors(): + with pytest.raises(ValueError) as exc: + APIData() + 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: + APIData(**{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: + APIData(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: + APIData(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: + APIData(primary_keys=['foo'], fields={}) + assert exc.value.args[0] == 'Primary key foo must be in fields!' + + with pytest.raises(ValueError) as exc: + APIData(stratify_keys=['foo'], fields={}) + assert exc.value.args[0] == 'Stratify key foo must be in fields!' + + with pytest.raises(ValueError) as exc: + APIData(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: + APIData(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: + APIData(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: + APIData(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' + + +SPLITTED_PATHS = [ + ('', [], ''), + (' ip ', ['ip'], 'ip'), + ('ip', ['ip'], 'ip'), + (' ip \t\n\raddress ', ['ip', 'address'], 'ip address'), +] + + +@pytest.mark.parametrize("joined_input, splitted, joined_output", SPLITTED_PATHS) +def test_join_split_path(joined_input, splitted, joined_output): + assert split_path(joined_input) == splitted + assert join_path(splitted) == joined_output diff --git a/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test_quoting.py b/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test_quoting.py new file mode 100644 index 000000000..6d29d507c --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/module_utils/test_quoting.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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/ansible_collections/community/routeros/tests/unit/plugins/modules/fake_api.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/fake_api.py new file mode 100644 index 000000000..a5ddb3180 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fake_api.py @@ -0,0 +1,243 @@ +# 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 + + +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): + path_info = PATHS[path] + 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 + self._path_info = PATHS[path] + 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 __iter__(self): + return [entry.copy() 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, + } + entry.update(kwargs) + _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') + 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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export new file mode 100644 index 000000000..d1ac49140 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export @@ -0,0 +1,24 @@ +# sep/25/2018 10:10:52 by RouterOS 6.42.5 +# software id = 9EER-511K +# +# +# +/interface wireless security-profiles +set [ find default=yes ] supplicant-identity=MikroTik +/tool user-manager customer +set admin access=own-routers,own-users,own-profiles,own-limits,config-payment-gw +/ip address +add address=192.168.88.1/24 comment=defconf interface=ether1 network=192.168.88.0 +/ip dhcp-client +add dhcp-options=hostname,clientid disabled=no interface=ether1 +/system lcd page +set time disabled=yes display-time=5s +set resources disabled=yes display-time=5s +set uptime disabled=yes display-time=5s +set packets disabled=yes display-time=5s +set bits disabled=yes display-time=5s +set version disabled=yes display-time=5s +set identity disabled=yes display-time=5s +set ether1 disabled=yes display-time=5s +/tool user-manager database +set db-path=user-manager diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose new file mode 100644 index 000000000..0f49fefe3 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose @@ -0,0 +1,26 @@ +# sep/25/2018 10:10:52 by RouterOS 6.42.5 +# software id = 9EER-511K +# +# +# +/interface wireless security-profiles +set [ find default=yes ] supplicant-identity=MikroTik +/tool user-manager customer +set admin access=own-routers,own-users,own-profiles,own-limits,config-payment-gw +/ip address +add address=192.168.88.1/24 comment=defconf interface=ether1 network=192.168.88.0 +/ip dhcp-client +add dhcp-options=hostname,clientid disabled=no interface=ether1 +/system lcd +set contrast=0 enabled=no port=parallel type=24x4 +/system lcd page +set time disabled=yes display-time=5s +set resources disabled=yes display-time=5s +set uptime disabled=yes display-time=5s +set packets disabled=yes display-time=5s +set bits disabled=yes display-time=5s +set version disabled=yes display-time=5s +set identity disabled=yes display-time=5s +set ether1 disabled=yes display-time=5s +/tool user-manager database +set db-path=user-manager diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/export_verbose.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging new file mode 100644 index 000000000..9ccddb292 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging @@ -0,0 +1,34 @@ +Flags: D - dynamic, X - disabled, R - running, S - slave + 0 R name="ether1" default-name="ether1" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:90 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 1 R name="ether2" default-name="ether2" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:91 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 2 R name="ether3" default-name="ether3" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:92 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 3 R name="ether4" default-name="ether4" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:93 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 4 R name="ether5" default-name="ether5" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:94 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 5 R name="ether6" default-name="ether6" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:95 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 6 R name="ether7" default-name="ether7" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:96 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 7 R name="ether8" default-name="ether8" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:97 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 8 R name="ether9" default-name="ether9" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:98 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 + 9 R name="ether10" default-name="ether10" type="ether" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:99 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 +10 R name="pppoe" default-name="pppoe" type="ppp" mtu=1500 actual-mtu=1500 + mac-address=00:1C:42:36:52:00 last-link-up-time=sep/25/2018 06:30:04 + link-downs=0 diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/interface_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging new file mode 100644 index 000000000..d4fd2bcd2 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging @@ -0,0 +1,10 @@ +Flags: X - disabled, I - invalid, D - dynamic + 0 ;;; defconf + address=192.168.88.1/24 network=192.168.88.0 interface=ether1 + actual-interface=ether1 + + 1 D address=10.37.129.3/24 network=10.37.129.0 interface=ether1 + actual-interface=ether1 + + 2 D address=10.37.0.0/24 network=10.37.0.1 interface=pppoe + actual-interface=pppoe diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_address_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging new file mode 100644 index 000000000..906dfb750 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging @@ -0,0 +1,15 @@ + 0 interface=ether2-master address=10.37.129.3 address4=10.37.129.3 mac-address=D4:CA:6D:C6:16:4C identity="router1" platform="MikroTik" version="6.42.2 (stable)" unpack=none age=59s + uptime=3w19h11m36s software-id="1234-1234" board="RBwAPG-5HacT2HnD" interface-name="bridge" system-description="MikroTik RouterOS 6.42.2 (stable) RBwAPG-5HacT2HnD" + system-caps="" system-caps-enabled="" + + 1 interface=ether3 address=10.37.129.4 address4=10.37.129.4 mac-address=D4:CA:6D:C6:18:2F identity="router2" platform="MikroTik" version="6.42.2 (stable)" unpack=none age=54s + uptime=3w19h11m30s software-id="1234-1234" board="RBwAPG-5HacT2HnD" ipv6=no interface-name="bridge" system-description="MikroTik RouterOS 6.42.2 (stable) RBwAPG-5HacT2HnD" + system-caps="" system-caps-enabled="" + + 2 interface=ether5 address=10.37.129.5 address4=10.37.129.5 mac-address=B8:69:F4:37:F0:C8 identity="router3" platform="MikroTik" version="6.40.8 (bugfix)" unpack=none age=43s + uptime=3d14h25m31s software-id="1234-1234" board="RB960PGS" interface-name="ether1" system-description="MikroTik RouterOS 6.40.8 (bugfix) RB960PGS" system-caps="" + system-caps-enabled="" + + 3 interface=ether10 address=10.37.129.6 address4=10.37.129.6 mac-address=6C:3B:6B:A1:0B:63 identity="router4" platform="MikroTik" version="6.42.2 (stable)" unpack=none age=54s + uptime=3w6d1h11m44s software-id="1234-1234" board="RBSXTLTE3-7" interface-name="bridge" system-description="MikroTik RouterOS 6.42.2 (stable) RBSXTLTE3-7" system-caps="" + system-caps-enabled="" diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_neighbor_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging new file mode 100644 index 000000000..6c2e558ef --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging @@ -0,0 +1,19 @@ +Flags: X - disabled, A - active, D - dynamic, +C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme, +B - blackhole, U - unreachable, P - prohibit + 0 ADC dst-address=10.10.66.0/30 pref-src=10.10.66.1 gateway=bridge1 + gateway-status=bridge1 reachable distance=0 scope=10 + routing-mark=altegro + + 2 A S dst-address=0.0.0.0/0 gateway=85.15.75.109 + gateway-status=85.15.75.109 reachable via Internet-VTK distance=1 + scope=30 target-scope=10 + + 3 ADC dst-address=10.10.1.0/30 pref-src=10.10.1.1 gateway=GRE_TYRMA + gateway-status=GRE_TYRMA reachable distance=0 scope=10 + + 4 DC dst-address=10.10.1.4/30 pref-src=10.10.1.5 gateway=RB2011 + gateway-status=RB2011 unreachable distance=255 scope=10 + + 5 ADC dst-address=10.10.2.0/30 pref-src=10.10.2.1 gateway=VLAN_SAT.ROUTER + gateway-status=VLAN_SAT.ROUTER reachable distance=0 scope=10 diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ip_route_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging new file mode 100644 index 000000000..c18e9ea57 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging @@ -0,0 +1,3 @@ +Flags: X - disabled, I - invalid, D - dynamic, G - global, L - link-local + 0 DL address=fe80::21c:42ff:fe36:5290/64 from-pool="" interface=ether1 + actual-interface=ether1 eui-64=no advertise=no no-dad=no diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6 b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6 new file mode 100644 index 000000000..0ea34bc0d --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6 @@ -0,0 +1 @@ +bad command name address (line 1 column 7) diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/ipv6_address_print_detail_without-paging_no-ipv6.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging new file mode 100644 index 000000000..8c560e2a4 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging @@ -0,0 +1,10 @@ +Flags: * - default, X - disabled + 0 *X name="default" as=65530 router-id=0.0.0.0 redistribute-connected=no + redistribute-static=no redistribute-rip=no redistribute-ospf=no + redistribute-other-bgp=no out-filter="" client-to-client-reflection=yes + ignore-as-path-len=no routing-table="" + + 1 name="MAIN_AS_STARKDV" as=64520 router-id=10.10.50.1 + redistribute-connected=no redistribute-static=no redistribute-rip=no + redistribute-ospf=no redistribute-other-bgp=no out-filter="" + client-to-client-reflection=yes ignore-as-path-len=no routing-table="" diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_instance_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging new file mode 100644 index 000000000..1ae4f5bcf --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging @@ -0,0 +1,13 @@ +Flags: X - disabled, E - established + 0 E name="iBGP_BRAS.TYRMA" instance=MAIN_AS_STARKDV remote-address=10.10.100.1 + remote-as=64520 tcp-md5-key="" nexthop-choice=default multihop=no + route-reflect=yes hold-time=3m ttl=default in-filter="" out-filter="" + address-families=ip,l2vpn,vpnv4 update-source=LAN_KHV + default-originate=never remove-private-as=no as-override=no passive=no + use-bfd=yes + + 1 E name="iBGP_BRAS_SAT" instance=MAIN_AS_STARKDV remote-address=10.10.50.230 + remote-as=64520 tcp-md5-key="" nexthop-choice=default multihop=no + route-reflect=yes hold-time=3m ttl=default in-filter="" out-filter="" + address-families=ip default-originate=never remove-private-as=no + as-override=no passive=no use-bfd=yes diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_peer_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging new file mode 100644 index 000000000..f64fa6d00 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging @@ -0,0 +1,7 @@ +Flags: L - label-present + 0 L route-distinguisher=64520:666 dst-address=10.10.66.8/30 gateway=10.10.100.1 + interface=GRE_TYRMA in-label=6136 out-label=6136 bgp-local-pref=100 + bgp-origin=incomplete bgp-ext-communities="RT:64520:666" + + 1 L route-distinguisher=64520:666 dst-address=10.10.66.0/30 interface=bridge1 + in-label=1790 bgp-ext-communities="RT:64520:666" diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_bgp_vpnv4-route_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging new file mode 100644 index 000000000..964046633 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging @@ -0,0 +1,10 @@ +Flags: X - disabled, * - default + 0 * name="default" router-id=10.10.50.1 distribute-default=never redistribute-connected=no + redistribute-static=no redistribute-rip=no redistribute-bgp=no redistribute-other-ospf=no + 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 + + 1 name="OSPF_ALTEGRO" router-id=10.10.66.1 distribute-default=never redistribute-connected=no + redistribute-static=no redistribute-rip=no redistribute-bgp=no redistribute-other-ospf=no + 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 routing-table=altegro diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_instance_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging new file mode 100644 index 000000000..d683b252c --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging @@ -0,0 +1,3 @@ +0 instance=default router-id=10.10.100.1 address=10.10.1.2 interface=GRE_TYRMA priority=1 + dr-address=0.0.0.0 backup-dr-address=0.0.0.0 state="Full" state-changes=15 ls-retransmits=0 + ls-requests=0 db-summaries=0 adjacency=6h8m46s diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/routing_ospf_neighbor_print_detail_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging new file mode 100644 index 000000000..d7dc3ff3e --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging @@ -0,0 +1 @@ + name: MikroTik diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_identity_print_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging new file mode 100644 index 000000000..79353f791 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging @@ -0,0 +1,16 @@ + uptime: 3h28m52s + version: 6.42.5 (stable) + build-time: Jun/26/2018 12:12:08 + free-memory: 988.3MiB + total-memory: 1010.8MiB + cpu: Intel(R) + cpu-count: 2 + cpu-frequency: 2496MHz + cpu-load: 0% + free-hdd-space: 63.4GiB + total-hdd-space: 63.5GiB + write-sect-since-reboot: 4576 + write-sect-total: 4576 + architecture-name: x86 + board-name: x86 + platform: MikroTik diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_resource_print_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging new file mode 100644 index 000000000..263c95909 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging @@ -0,0 +1,7 @@ + routerboard: yes + model: RouterBOARD 3011UiAS + serial-number: 1234567890 + firmware-type: ipq8060 + factory-firmware: 3.41 + current-firmware: 3.41 + upgrade-firmware: 6.42.2 diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/facts/system_routerboard_print_without-paging.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print new file mode 100644 index 000000000..3f806211e --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + MMM MMM KKK TTTTTTTTTTT KKK + + MMMM MMMM KKK TTTTTTTTTTT KKK + + MMM MMMM MMM III KKK KKK RRRRRR OOOOOO TTT III KKK KKK + + MMM MM MMM III KKKKK RRR RRR OOO OOO TTT III KKKKK + + MMM MMM III KKK KKK RRRRRR OOO OOO TTT III KKK KKK + + MMM MMM III KKK KKK RRR RRR OOOOOO TTT III KKK KKK + + + + MikroTik RouterOS 6.42.5 (c) 1999-2018 http://www.mikrotik.com/ + + +[?] Gives the list of available commands + +command [?] Gives help on the command and list of arguments + + + +[Tab] Completes the command/word. If the input is ambiguous, + + a second [Tab] gives possible options + + + +/ Move up to base level + +.. Move up one level + +/command Use command at the base level + +[9999B +[9999BZ [6n<[c[4l[20l[?47l[?7h[?5l[?25h[H[9999B[6n + + + +[admin@MainRouter] > +[admin@MainRouter] > /system routerboard print +[admin@MainRouter] > /system routerboard print + + routerboard: yes + model: 750GL + serial-number: 1234567890AB + firmware-type: ar7240 + factory-firmware: 3.09 + current-firmware: 6.41.2 + upgrade-firmware: 6.42.5 + + + + + +[admin@MainRouter] > +[admin@MainRouter] > /system identity print +[admin@MainRouter] > /system identity print + + name: MikroTik + + + + + +[admin@MainRouter] > +[admin@MainRouter] > /system package print +[admin@MainRouter] > /system package print + +Flags: [m[1mX[m - disabled +[m[1m # NAME VERSION SCHEDULED +[m 0 routeros-mipsbe 6.42.5 + 1 system 6.42.5 + 2 ipv6 6.42.5 + 3 wireless 6.42.5 + 4 hotspot 6.42.5 + 5 dhcp 6.42.5 + 6 mpls 6.42.5 + 7 routing 6.42.5 + 8 ppp 6.42.5 + 9 security 6.42.5 +10 advanced-tools 6.42.5 + + + + + +[admin@MainRouter] > +[admin@MainRouter] >
\ No newline at end of file diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_package_print.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print new file mode 100644 index 000000000..63bc3beba --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print @@ -0,0 +1,17 @@ +[admin@RB1100test] /system resource> print + uptime: 2w1d23h34m57s + version: "5.0rc1" + free-memory: 385272KiB + total-memory: 516708KiB + cpu: "e500v2" + cpu-count: 1 + cpu-frequency: 799MHz + cpu-load: 9% + free-hdd-space: 466328KiB + total-hdd-space: 520192KiB + write-sect-since-reboot: 1411 + write-sect-total: 70625 + bad-blocks: 0.2% + architecture-name: "powerpc" + board-name: "RB1100" + platform: "MikroTik" diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print.license b/ansible_collections/community/routeros/tests/unit/plugins/modules/fixtures/system_resource_print.license new file mode 100644 index 000000000..edff8c768 --- /dev/null +++ b/ansible_collections/community/routeros/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/ansible_collections/community/routeros/tests/unit/plugins/modules/routeros_module.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/routeros_module.py new file mode 100644 index 000000000..0ec44f70b --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/routeros_module.py @@ -0,0 +1,75 @@ +# 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) +__metaclass__ = type + +import os +import json + +from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase + + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except Exception: + pass + + fixture_data[path] = data + return data + + +class TestRouterosModule(ModuleTestCase): + + def execute_module(self, failed=False, changed=False, commands=None, sort=True, defaults=False): + + self.load_fixtures(commands) + + if failed: + result = self.failed() + self.assertTrue(result['failed'], result) + else: + result = self.changed(changed) + self.assertEqual(result['changed'], changed, result) + + if commands is not None: + if sort: + self.assertEqual(sorted(commands), sorted(result['commands']), result['commands']) + else: + self.assertEqual(commands, result['commands'], result['commands']) + + return result + + def failed(self): + with self.assertRaises(AnsibleFailJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertTrue(result['failed'], result) + return result + + def changed(self, changed=False): + with self.assertRaises(AnsibleExitJson) as exc: + self.module.main() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], changed, result) + return result + + def load_fixtures(self, commands=None): + pass diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api.py new file mode 100644 index 000000000..4cfdeef1e --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api.py @@ -0,0 +1,308 @@ +# 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, MagicMock +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, Or, fake_ros_api +from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase +from ansible_collections.community.routeros.plugins.modules import api + + +class TestRouterosApiModule(ModuleTestCase): + + def setUp(self): + super(TestRouterosApiModule, self).setUp() + 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() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + + @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() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_add(self): + 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() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_add_already_exist(self): + 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() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'][0], 'failure: already have interface with such name') + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_remove(self): + 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() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_remove_no_id(self): + 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() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'][0], 'no such item (4)') + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.arbitrary) + def test_api_cmd(self): + 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() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], False) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api.arbitrary) + def test_api_cmd_none_existing_cmd(self): + 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() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'][0], 'no such command') + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_update(self): + 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() + + result = exc.exception.args[0] + self.assertEqual(result['changed'], True) + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_update_none_existing_id(self): + 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() + + result = exc.exception.args[0] + self.assertEqual(result['failed'], True) + self.assertEqual(result['msg'][0], 'no such item (4)') + + @patch('ansible_collections.community.routeros.plugins.modules.api.ROS_api_module.api_add_path', new=fake_ros_api) + def test_api_query(self): + 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() + + 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() + + 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() + + 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() + + 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'], + } + 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'], + } + 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', + }, + ], + } + 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', + }, + ], + } + 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, + }, + ], + }, + ], + } + 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/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_facts.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_facts.py new file mode 100644 index 000000000..64985f8b6 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_facts.py @@ -0,0 +1,752 @@ +# 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, MagicMock +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api +from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase +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: + 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'] + 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: + 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/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_find_and_modify.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_find_and_modify.py new file mode 100644 index 000000000..384bc8885 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_find_and_modify.py @@ -0,0 +1,651 @@ +# 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, MagicMock +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.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase +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) + + +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: + 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', + }, + }) + 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', + }, + }) + 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, + }, + }) + 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', + }, + }) + 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, + }) + 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, + }) + 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, + }) + 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', + }, + }) + 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, + }) + 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': { + }, + }) + 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, + }, + }) + 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, + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + }, + { + '.id': '*7', + 'comment': 'bar', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + }, + ]) + self.assertEqual(result['diff']['before']['values'], [ + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + }, + ]) + self.assertEqual(result['diff']['after']['values'], [ + { + '.id': '*7', + 'comment': 'bar', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': 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, + }, + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': 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': '', + }, + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': 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, + }, + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + 'dynamic': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + 'dynamic': 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, + }, + }) + 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', + }, + { + '.id': '*3', + 'action': 'accept', + 'chain': 'input', + 'comment': 'defconf', + }, + { + '.id': '*4', + 'action': 'accept', + 'chain': 'input', + 'comment': 'defconf', + }, + { + '.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', + }, + ]) + self.assertEqual(result['match_count'], 3) + self.assertEqual(result['modify_count'], 2) diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_info.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_info.py new file mode 100644 index 000000000..2dabc36ef --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_info.py @@ -0,0 +1,811 @@ +# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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, MagicMock +from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api +from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase +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.module.Key = MagicMock(new=Key) + 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: + 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' + }) + 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' + }) + 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', + }) + 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, + }) + 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, + }) + 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', + }) + 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, + '!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, + '!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', + }) + 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, + '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, + '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', + }) + 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, + }) + 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', + }) + 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', + 'include': '', + 'exclude': '', + '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, + }) + 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', + 'include': '', + 'exclude': '', + 'builtin': True, + 'comment': 'contains all interfaces', + }, + { + '.id': '*2000001', + 'name': 'none', + 'include': '', + 'exclude': '', + 'builtin': True, + 'comment': 'contains no interfaces', + }, + { + '.id': '*2000010', + 'name': 'WAN', + 'include': '', + 'exclude': '', + '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', + }) + 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', + }) + 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, + }) + 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', + }, + ]) diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_modify.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_modify.py new file mode 100644 index 000000000..78979733d --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_api_modify.py @@ -0,0 +1,1972 @@ +# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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, MagicMock +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.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase +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.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: + 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': [], + }) + 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', + }], + }) + 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, + }], + }) + 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, + }], + }) + 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', + }], + }) + 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, + }], + }) + 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', + }], + }) + 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', + }], + }) + 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', + }], + }) + 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', + }, + ], + }) + 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', + }) + 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', + }, + ], + }) + 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', + }, + ], + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*NEW1', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': 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', + }, + ], + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*NEW1', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': 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, + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': 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', + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': 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, + }) + 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, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': 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', + }) + 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, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*NEW1', + 'name': 'router', + 'cname': 'router.com.', + 'ttl': '1d', + 'disabled': 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, + }) + 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, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': 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', + }) + 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, + }, + { + '.id': '*A', + 'comment': 'defconf', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': 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, + }) + 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, + }, + { + '.id': '*A', + 'comment': 'defconf', + 'name': 'router', + 'text': 'Router Text Entry 2', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*7', + 'name': 'foo', + 'address': '192.168.88.2', + 'ttl': '1d', + 'disabled': 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', + }) + 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, + }, + ]) + + @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, + }) + 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, + }, + ]) + + @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, + }) + 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, + }, + { + '.id': '*NEW1', + 'name': 'foo', + 'text': 'bar', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': 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, + }) + 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, + }, + { + 'name': 'foo', + 'text': 'bar', + }, + { + '.id': '*A', + 'name': 'router', + 'text': 'Router Text Entry', + 'ttl': '1d', + 'disabled': False, + }, + { + '.id': '*1', + 'comment': 'defconf', + 'name': 'router', + 'address': '192.168.88.1', + 'ttl': '1d', + 'disabled': 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, + }, + ], + }) + 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', + }) + 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, + }, + ], + }) + 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, + }) + 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', + }) + 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, + }) + 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, + }, + ], + }) + 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', + }) + 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', + }) + 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, + }) + 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, + }) + 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, + }) + 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, + }) + 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, + }) + 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, + }) + 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, + }) + 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, + }) + 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/ansible_collections/community/routeros/tests/unit/plugins/modules/test_command.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_command.py new file mode 100644 index 000000000..3fc586586 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_command.py @@ -0,0 +1,100 @@ +# 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) +__metaclass__ = type + +import json + +from ansible_collections.community.routeros.tests.unit.compat.mock import patch +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 + + +class TestRouterosCommandModule(TestRouterosModule): + + module = command + + def setUp(self): + super(TestRouterosCommandModule, self).setUp() + + self.mock_run_commands = patch('ansible_collections.community.routeros.plugins.modules.command.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + super(TestRouterosCommandModule, self).tearDown() + self.mock_run_commands.stop() + + def load_fixtures(self, commands=None): + + def load_from_file(*args, **kwargs): + module, commands = args + output = list() + + for item in commands: + try: + obj = json.loads(item) + command = obj + except ValueError: + command = item + filename = str(command).replace(' ', '_').replace('/', '') + output.append(load_fixture(filename)) + return output + + 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(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(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(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) + 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) + 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(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(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) + + 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(changed=True) diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/test_facts.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_facts.py new file mode 100644 index 000000000..322fdda45 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/test_facts.py @@ -0,0 +1,335 @@ +# 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.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 + + +class TestRouterosFactsModule(TestRouterosModule): + + module = facts + + def setUp(self): + super(TestRouterosFactsModule, self).setUp() + self.mock_run_commands = patch('ansible_collections.community.routeros.plugins.modules.facts.run_commands') + self.run_commands = self.mock_run_commands.start() + + def tearDown(self): + super(TestRouterosFactsModule, self).tearDown() + self.mock_run_commands.stop() + + def load_fixtures(self, commands=None): + def load_from_file(*args, **kwargs): + module = args + commands = kwargs['commands'] + output = list() + + for command in commands: + 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() + self.assertEqual( + result['ansible_facts']['ansible_net_hostname'], 'MikroTik' + ) + self.assertEqual( + result['ansible_facts']['ansible_net_version'], '6.42.5 (stable)' + ) + self.assertEqual( + result['ansible_facts']['ansible_net_model'], 'RouterBOARD 3011UiAS' + ) + self.assertEqual( + result['ansible_facts']['ansible_net_serialnum'], '1234567890' + ) + self.assertEqual( + result['ansible_facts']['ansible_net_arch'], 'x86' + ) + self.assertEqual( + result['ansible_facts']['ansible_net_uptime'], '3h28m52s' + ) + + def test_facts_hardware(self): + set_module_args(dict(gather_subset='hardware')) + result = self.execute_module() + self.assertEqual( + result['ansible_facts']['ansible_net_spacefree_mb'], 64921.6 + ) + self.assertEqual( + result['ansible_facts']['ansible_net_spacetotal_mb'], 65024.0 + ) + self.assertEqual( + result['ansible_facts']['ansible_net_memfree_mb'], 988.3 + ) + self.assertEqual( + result['ansible_facts']['ansible_net_memtotal_mb'], 1010.8 + ) + + def test_facts_config(self): + set_module_args(dict(gather_subset='config')) + result = self.execute_module() + self.assertIsInstance( + result['ansible_facts']['ansible_net_config'], str + ) + + self.assertIsInstance( + result['ansible_facts']['ansible_net_config_nonverbose'], str + ) + + def test_facts_interfaces(self): + 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'] + ) + self.assertEqual( + result['ansible_facts']['ansible_net_all_ipv6_addresses'], ['fe80::21c:42ff:fe36:5290'] + ) + self.assertEqual( + result['ansible_facts']['ansible_net_all_ipv6_addresses'][0], + result['ansible_facts']['ansible_net_interfaces']['ether1']['ipv6'][0]['address'] + ) + self.assertEqual( + len(result['ansible_facts']['ansible_net_interfaces'].keys()), 11 + ) + self.assertEqual( + len(result['ansible_facts']['ansible_net_neighbors']), 4 + ) + + def test_facts_interfaces_no_ipv6(self): + fixture = load_fixture( + 'facts/ipv6_address_print_detail_without-paging_no-ipv6' + ) + interfaces = self.module.Interfaces(module=self.module) + addresses = interfaces.parse_detail(data=fixture) + result = interfaces.populate_addresses(data=addresses, family='ipv6') + + self.assertEqual(result, None) + + def test_facts_routing(self): + 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'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['instance'], ['MAIN_AS_STARKDV'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['remote-address'], ['10.10.100.1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['remote-as'], ['64520'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['nexthop-choice'], ['default'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['multihop'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['route-reflect'], ['yes'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['hold-time'], ['3m'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['ttl'], ['default'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['address-families'], ['ip'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['update-source'], ['LAN_KHV'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['default-originate'], ['never'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['remove-private-as'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['as-override'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['passive'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_peer']['iBGP_BRAS.TYRMA']['use-bfd'], ['yes'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['route-distinguisher'], ['64520:666'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['dst-address'], ['10.10.66.8/30'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['gateway'], ['10.10.100.1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['interface'], ['GRE_TYRMA'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['in-label'], ['6136'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['out-label'], ['6136'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['bgp-local-pref'], ['100'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['bgp-origin'], ['incomplete'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_vpnv4_route']['GRE_TYRMA']['bgp-ext-communities'], ['RT:64520:666'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['name'], ['default'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['as'], ['65530'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['router-id'], ['0.0.0.0'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-connected'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-static'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-rip'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-ospf'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['redistribute-other-bgp'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['client-to-client-reflection'], ['yes'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_bgp_instance']['default']['ignore-as-path-len'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_route']['altegro']['dst-address'], ['10.10.66.0/30'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_route']['altegro']['pref-src'], ['10.10.66.1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_route']['altegro']['gateway'], ['bridge1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_route']['altegro']['gateway-status'], ['bridge1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_route']['altegro']['distance'], ['0'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_route']['altegro']['scope'], ['10'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_route']['altegro']['routing-mark'], ['altegro'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['name'], ['default'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['router-id'], ['10.10.50.1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['distribute-default'], ['never'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-connected'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-static'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-rip'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-bgp'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['redistribute-other-ospf'], ['no'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-default'], ['1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-connected'], ['20'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-static'], ['20'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-rip'], ['20'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-bgp'], ['auto'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['metric-other-ospf'], ['auto'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['in-filter'], ['ospf-in'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_instance']['default']['out-filter'], ['ospf-out'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['instance'], ['default'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['router-id'], ['10.10.100.1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['address'], ['10.10.1.2'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['interface'], ['GRE_TYRMA'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['priority'], ['1'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['dr-address'], ['0.0.0.0'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['backup-dr-address'], ['0.0.0.0'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['state'], ['Full'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['state-changes'], ['15'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['ls-retransmits'], ['0'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['ls-requests'], ['0'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['db-summaries'], ['0'] + ) + self.assertIn( + result['ansible_facts']['ansible_net_ospf_neighbor']['default']['adjacency'], ['6h8m46s'] + ) diff --git a/ansible_collections/community/routeros/tests/unit/plugins/modules/utils.py b/ansible_collections/community/routeros/tests/unit/plugins/modules/utils.py new file mode 100644 index 000000000..419eef114 --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/plugins/modules/utils.py @@ -0,0 +1,54 @@ +# 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 + +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/ansible_collections/community/routeros/tests/unit/requirements.txt b/ansible_collections/community/routeros/tests/unit/requirements.txt new file mode 100644 index 000000000..479f2fc6f --- /dev/null +++ b/ansible_collections/community/routeros/tests/unit/requirements.txt @@ -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 + +unittest2 ; python_version <= '2.6' +ordereddict ; python_version <= '2.6' diff --git a/ansible_collections/community/routeros/tests/unit/requirements.yml b/ansible_collections/community/routeros/tests/unit/requirements.yml new file mode 100644 index 000000000..6a22736b5 --- /dev/null +++ b/ansible_collections/community/routeros/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: +- ansible.netcommon diff --git a/ansible_collections/community/routeros/update-docs.py b/ansible_collections/community/routeros/update-docs.py new file mode 100755 index 000000000..17a431b05 --- /dev/null +++ b/ansible_collections/community/routeros/update-docs.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de> +# 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. +''' + +from plugins.module_utils._api_data import ( + PATHS, + join_path, +) + + +MODULES = [ + 'plugins/modules/api_info.py', + 'plugins/modules/api_modify.py', +] + + +def update_file(file, begin_line, end_line, choice_line, path_choices): + 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: + print(f'{file} has been updated') + with open(file, 'w', encoding='utf-8') as f: + f.write('\n'.join(new_lines) + '\n') + + +def main(): + path_choices = sorted([join_path(path) for path, path_info in PATHS.items() if path_info.fully_understood]) + + for file in MODULES: + update_file(file, ' # BEGIN PATH LIST', ' # END PATH LIST', ' - {choice}', path_choices) + + +if __name__ == '__main__': + main() |