diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..fc7bd29 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +roles_path = roles +collections_paths = collections +retry_files_enabled = False +#stdout_callback = yaml +bin_ansible_callbacks = True +interpreter_python = auto_silent +host_key_checking = False +deprecation_warnings = False + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null +pipelining = True diff --git a/chrony.readme.md b/chrony.readme.md new file mode 100644 index 0000000..553e053 --- /dev/null +++ b/chrony.readme.md @@ -0,0 +1,60 @@ +# **Chrony** — это современная реализация протокола **NTP** (Network Time Protocol). + +Простыми словами, это сервис, который синхронизирует время на вашем сервере с точными атомными часами через интернет или локальную сеть. + +В современных дистрибутивах (CentOS 7+, RHEL, Ubuntu 18.04+) он идет по умолчанию + +- **Быстрая синхронизация:** Chrony гораздо быстрее корректирует время при загрузке системы, чем старый `ntpd`. +- **Работа с нестабильным интернетом:** Он отлично справляется, если связь с серверами времени часто пропадает (например, на виртуальных машинах). +- **Плавная подстройка:** Если время «убежало», Chrony не просто резко переводит часы (что может сломать работу софта), а незаметно ускоряет или замедляет ход системных часов, пока время не выровняется. +- **Минимум ресурсов:** Потребляет очень мало оперативной памяти и процессора. + +Для разворачивания и использования созданы 2 плейбука: + +**chrony-ntp-client**.yml для установки в качестве клиента + +```plaintext +chrony_disable_ntpd: true # отключаем встроенный ntpd (если есть) +chrony_enable: true # Включаем сервис, запускаем +chrony_log_enable: true # Включаем логи +chrony_log: measurements statistics tracking # Какие логи будем писать +chrony_logdir: /var/log/chrony/ # Указываем путь для логов +chrony_ntp_servers: # На какие сервера будем ходить за временем +- ntp01.gigacoms.info +- ntp02.gigacoms.info +chrony_rtcsync: true # Меняем часы локалхоста +chrony_timezone: "Europe/Moscow" # Указываем таймзону +``` + +**chrony-ntp-server**.yml для сервера + +```plaintext +chrony_allow_enable: true # переходим в режим сервера +chrony_allow: # ACL каким сетям дозволяется обращаться за временем + - ::/0 + - 127.0.0.0/8 + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 100.64.0.0/10 + - 91.218.84.0/22 + - 91.236.140.0/22 + - 91.197.57.0/24 + - 213.109.201.0/24 + - 46.255.25.0/24 + - 2a13:af80::/29 + +chrony_disable_ntpd: true # Отключаем ntpd если есть +chrony_enable: true +chrony_log_enable: true +chrony_log: measurements statistics tracking +chrony_logdir: /var/log/chrony/ +chrony_ntp_servers: # Источники времени рекомендуется использовать ближайшие. + - 0.ru.pool.ntp.org # сервера из ru.pool.ntp.org + - 1.ru.pool.ntp.org + - 2.ru.pool.ntp.org + - 3.ru.pool.ntp.org +chrony_rtcsync: true # Синкаем часы локалхоста +chrony_smearing: true # Leap Smearing — это специальный механизм обработки «високосной секунды» (Leap Second), которую периодически добавляют к мировому времени, чтобы компенсировать замедление вращения Земли. +chrony_timezone: "Europe/Moscow" # Устанавливаем таймзону +``` \ No newline at end of file diff --git a/collections/ansible_collections/ansible.netcommon-8.5.0.info/GALAXY.yml b/collections/ansible_collections/ansible.netcommon-8.5.0.info/GALAXY.yml new file mode 100644 index 0000000..a40835b --- /dev/null +++ b/collections/ansible_collections/ansible.netcommon-8.5.0.info/GALAXY.yml @@ -0,0 +1,8 @@ +download_url: https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/ansible-netcommon-8.5.0.tar.gz +format_version: 1.0.0 +name: netcommon +namespace: ansible +server: https://galaxy.ansible.com/api/ +signatures: [] +version: 8.5.0 +version_url: /api/v3/plugin/ansible/content/published/collections/index/ansible/netcommon/versions/8.5.0/ diff --git a/collections/ansible_collections/ansible.utils-6.0.2.info/GALAXY.yml b/collections/ansible_collections/ansible.utils-6.0.2.info/GALAXY.yml new file mode 100644 index 0000000..e00a425 --- /dev/null +++ b/collections/ansible_collections/ansible.utils-6.0.2.info/GALAXY.yml @@ -0,0 +1,8 @@ +download_url: https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/ansible-utils-6.0.2.tar.gz +format_version: 1.0.0 +name: utils +namespace: ansible +server: https://galaxy.ansible.com/api/ +signatures: [] +version: 6.0.2 +version_url: /api/v3/plugin/ansible/content/published/collections/index/ansible/utils/versions/6.0.2/ diff --git a/collections/ansible_collections/ansible/netcommon/.github/release-drafter.yml b/collections/ansible_collections/ansible/netcommon/.github/release-drafter.yml new file mode 100644 index 0000000..e3e5966 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/.github/release-drafter.yml @@ -0,0 +1,3 @@ +--- +# see https://github.com/ansible-community/devtools +_extends: ansible-community/devtools diff --git a/collections/ansible_collections/ansible/netcommon/.github/workflows/check_label.yml b/collections/ansible_collections/ansible/netcommon/.github/workflows/check_label.yml new file mode 100644 index 0000000..ef12980 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/.github/workflows/check_label.yml @@ -0,0 +1,11 @@ +--- +name: "Check PR label 👓" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +on: # yamllint disable-line rule:truthy + pull_request_target: + types: [opened, labeled, unlabeled, synchronize] +jobs: + check_label: + uses: ansible/ansible-content-actions/.github/workflows/check_label.yaml@main diff --git a/collections/ansible_collections/ansible/netcommon/.github/workflows/draft_release.yml b/collections/ansible_collections/ansible/netcommon/.github/workflows/draft_release.yml new file mode 100644 index 0000000..cb1a77f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/.github/workflows/draft_release.yml @@ -0,0 +1,18 @@ +--- +name: "Draft release" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +on: # yamllint disable-line rule:truthy + workflow_dispatch: +env: + NAMESPACE: ${{ github.repository_owner }} + COLLECTION_NAME: netcommon + ANSIBLE_COLLECTIONS_PATHS: ./ +jobs: + update_release_draft: + uses: ansible/ansible-content-actions/.github/workflows/draft_release.yaml@main + with: + repo: ${{ github.event.pull_request.head.repo.full_name }} + secrets: + BOT_PAT: ${{ secrets.BOT_PAT }} diff --git a/collections/ansible_collections/ansible/netcommon/.github/workflows/integration-cml.yml b/collections/ansible_collections/ansible/netcommon/.github/workflows/integration-cml.yml new file mode 100644 index 0000000..7dcd2fe --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/.github/workflows/integration-cml.yml @@ -0,0 +1,267 @@ +--- +name: "Integration tests 💻" +"on": + pull_request_target: + branches: [main] + types: + - labeled + - opened + - reopened + - synchronize + paths: + - "plugins/**" + - "tests/integration/**" + workflow_dispatch: + inputs: + ansible_version: + description: "Ansible core version to test" + required: true + type: choice + options: + - "2.15" + - "2.16" + - "2.17" + - "2.18" + - "2.19" + - "2.20" + - "milestone" + - "devel" + default: "devel" + debug: + description: "Enable debug steps" + required: false + type: boolean + default: false + ssh_type: + description: "network_cli SSH transport (paramiko or libssh)" + required: false + type: choice + options: + - "libssh" + - "paramiko" + default: "libssh" +jobs: + lab-create: + uses: ansible/ansible-content-actions/.github/workflows/cml_lab_create.yaml@main + with: + topology_path: tests/integration/labs/multi.yaml + # lab_title_override: "Optional custom lab title" + secrets: + virl_host: ${{ secrets.VIRL_HOST }} + virl_username: ${{ secrets.VIRL_USERNAME }} + virl_password: ${{ secrets.VIRL_PASSWORD }} + + integration-tests: + needs: lab-create + name: "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ssh_type || 'libssh' }} & ansible-core ${{ matrix.ansible_version }}" + runs-on: ubuntu-latest + if: ${{ !github.event.pull_request || contains(github.event.pull_request.labels.*.name, 'safe to test') }} + env: + PY_COLORS: "1" + ANSIBLE_COLLECTIONS_PATHS: "/home/runner/.ansible/collections" + # workflow_dispatch: use chosen ssh_type; PR/schedule: default libssh + CLI_SSH_TYPE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ssh_type || 'libssh' }} + strategy: + fail-fast: true + matrix: # There is no official support for matrix here as multiple applaince are needed per run, test against one core version. + ansible_version: ${{ github.event_name == 'workflow_dispatch' && fromJSON(format('["{0}"]', github.event.inputs.ansible_version)) || fromJSON('["devel"]') }} + steps: + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: DEBUG Show pip list before installs + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug == 'true' || github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'debug') }} + run: | + python3 -m pip --version + python3 -m pip list | sort + + - name: Upgrade pip and wheel + run: python3 -m pip install --upgrade pip wheel ansible-pylibssh paramiko genie passlib scp ncclient ntc_templates pyats jxmlease ssh-python + + - name: DEBUG Show pip list after base deps install + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug == 'true' || github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'debug') }} + run: | + python3 -m pip list | sort + + - name: Install ansible-core (${{ matrix.ansible_version }}) - Stable version + run: python3 -m pip install "ansible-core~=${{ matrix.ansible_version }}.0" --disable-pip-version-check + if: ${{ !contains(matrix.ansible_version, 'devel') && !contains(matrix.ansible_version, 'milestone') }} + + - name: Checkout ansible-core (${{ matrix.ansible_version }}) - Branch + uses: actions/checkout@v4 + with: + repository: ansible/ansible + ref: ${{ matrix.ansible_version }} + path: ansible-core-source + if: ${{ contains(matrix.ansible_version, 'devel') || contains(matrix.ansible_version, 'milestone') }} + + - name: Install ansible-core (${{ matrix.ansible_version }}) - Branch + run: python3 -m pip install ./ansible-core-source --disable-pip-version-check + if: ${{ contains(matrix.ansible_version, 'devel') || contains(matrix.ansible_version, 'milestone') }} + + - name: Create collections directory structure + run: | + mkdir -p /home/runner/.ansible/collections/ansible_collections/cisco + mkdir -p /home/runner/.ansible/collections/ansible_collections/ansible + + - name: Clone cisco.nxos collection (main branch) + run: | + git clone --depth=1 --branch main https://github.com/ansible-collections/cisco.nxos.git /home/runner/.ansible/collections/ansible_collections/cisco/nxos + + - name: Clone cisco.ios collection (main branch) + run: | + git clone --depth=1 --branch main https://github.com/ansible-collections/cisco.ios.git /home/runner/.ansible/collections/ansible_collections/cisco/ios + + - name: Clone cisco.iosxr collection (main branch) + run: | + git clone --depth=1 --branch main https://github.com/ansible-collections/cisco.iosxr.git /home/runner/.ansible/collections/ansible_collections/cisco/iosxr + + - name: Clone ansible.utils collection (main branch) + run: | + git clone --depth=1 --branch main https://github.com/ansible-collections/ansible.utils.git /home/runner/.ansible/collections/ansible_collections/ansible/utils + + - name: Clone netcommon collection (PR branch or repo) + run: | + if [ -n "${{ github.event.pull_request.head.repo.full_name }}" ]; then + REPO_URL="${{ github.event.pull_request.head.repo.full_name }}" + BRANCH_REF="${{ github.event.pull_request.head.sha }}" + IS_SHA=true + else + REPO_URL="${{ github.repository }}" + BRANCH_REF="${{ github.ref_name }}" + IS_SHA=false + fi + echo "Cloning netcommon from git+https://github.com/${REPO_URL}.git, ref: ${BRANCH_REF}" + if [ "$IS_SHA" = "true" ]; then + # For SHA, clone with more depth to ensure we get the commit + git clone --depth=100 --no-single-branch https://github.com/${REPO_URL}.git /home/runner/.ansible/collections/ansible_collections/ansible/netcommon + cd /home/runner/.ansible/collections/ansible_collections/ansible/netcommon + git fetch origin ${BRANCH_REF} || git fetch --unshallow || true + git checkout ${BRANCH_REF} + else + # For branch name, use standard shallow clone + git clone --depth=1 --branch ${BRANCH_REF} https://github.com/${REPO_URL}.git /home/runner/.ansible/collections/ansible_collections/ansible/netcommon + fi + + - name: Install Python dependencies from collections + run: | + if [ -f "/home/runner/.ansible/collections/ansible_collections/cisco/nxos/requirements.txt" ]; then + python3 -m pip install -r /home/runner/.ansible/collections/ansible_collections/cisco/nxos/requirements.txt + fi + if [ -f "/home/runner/.ansible/collections/ansible_collections/ansible/netcommon/requirements.txt" ]; then + python3 -m pip install -r /home/runner/.ansible/collections/ansible_collections/ansible/netcommon/requirements.txt + fi + + - name: DEBUG Show pip list after ansible-core install + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug == 'true' || github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'debug') }} + run: | + python3 -m pip list | sort + + - name: Read collection metadata from galaxy.yml + id: identify + uses: ansible-network/github_actions/.github/actions/identify_collection@main + with: + source_path: /home/runner/.ansible/collections/ansible_collections/ansible/netcommon + + - name: DEBUG lab_nodes input + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug == 'true' || github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'debug') }} + shell: bash + env: + LAB_NODES: ${{ needs.lab-create.outputs.lab_nodes_json }} + run: | + echo 'LAB_NODES (raw):' + echo "$LAB_NODES" + echo + + - name: Generate inventory from lab_nodes and template + id: render_inventory + shell: bash + working-directory: ${{ steps.identify.outputs.collection_path }} + env: + LAB_NODES_JSON: ${{ needs.lab-create.outputs.lab_nodes_json }} + RUNNER_TEMP: ${{ runner.temp }} + CLI_SSH_TYPE: ${{ env.CLI_SSH_TYPE }} + run: | + set -euo pipefail + if ! command -v jq >/dev/null 2>&1; then + sudo apt-get update -y >/dev/null 2>&1 || true + sudo apt-get install -y jq >/dev/null 2>&1 || true + fi + # Extract DHCP IPs for each node + nxos_ip=$(echo "$LAB_NODES_JSON" | jq -r '.nodes.nxos // empty') + iosxr_ip=$(echo "$LAB_NODES_JSON" | jq -r '.nodes.iosxr // empty') + iosxe_ip=$(echo "$LAB_NODES_JSON" | jq -r '.nodes.iosxe // empty') + ubuntu_ip=$(echo "$LAB_NODES_JSON" | jq -r '.nodes.ubuntu // empty') + + if [ -z "$nxos_ip" ] || [ -z "$iosxr_ip" ] || [ -z "$iosxe_ip" ] || [ -z "$ubuntu_ip" ]; then + echo "Missing one or more required nodes (nxos, iosxr, iosxe, ubuntu) in LAB_NODES_JSON" + echo "Available keys:" $(echo "$LAB_NODES_JSON" | jq -r '.nodes | keys | join(", ")') + exit 1 + fi + + # Compute ports from last octet of each DHCP IP + nxos_last_octet="${nxos_ip##*.}" + nxos_ssh_port=$((nxos_last_octet + 2000)) + nxos_https_port=$((nxos_last_octet + 4000)) + nxos_http_port=$((nxos_last_octet + 8000)) + nxos_netconf_port=$((nxos_last_octet + 3000)) + + iosxr_last_octet="${iosxr_ip##*.}" + iosxr_ssh_port=$((iosxr_last_octet + 2000)) + iosxr_https_port=$((iosxr_last_octet + 4000)) + iosxr_http_port=$((iosxr_last_octet + 8000)) + iosxr_netconf_port=$((iosxr_last_octet + 3000)) + iosxr_grpc_port=57400 + + iosxe_last_octet="${iosxe_ip##*.}" + iosxe_ssh_port=$((iosxe_last_octet + 2000)) + iosxe_https_port=$((iosxe_last_octet + 4000)) + iosxe_http_port=$((iosxe_last_octet + 8000)) + iosxe_netconf_port=$((iosxe_last_octet + 3000)) + + ubuntu_last_octet="${ubuntu_ip##*.}" + ubuntu_ssh_port=$((ubuntu_last_octet + 2000)) + + # Destination host for port-forwarded access + dest_host="54.190.208.146" + out_file="$RUNNER_TEMP/inventory-${{ matrix.ansible_version }}.ini" + ansible localhost -c local -m template -a "src=tests/integration/labs/inventory.j2 dest=$out_file mode=0644" \ + -e "ansible_host=$dest_host ansible_network_cli_ssh_type=$CLI_SSH_TYPE \ + nxos_ssh_port=$nxos_ssh_port nxos_http_port=$nxos_http_port nxos_netconf_port=$nxos_netconf_port \ + iosxr_netconf_port=$iosxr_netconf_port iosxr_grpc_port=$iosxr_grpc_port ubuntu_ssh_port=$ubuntu_ssh_port \ + iosxe_http_port=$iosxe_http_port iosxr_ssh_port=$iosxr_ssh_port iosxe_ssh_port=$iosxe_ssh_port" + echo "inventory_path=$out_file" >> "$GITHUB_OUTPUT" + + - name: DEBUG Print the ansible version + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug == 'true' || github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'debug') }} + run: ansible --version + + - name: DEBUG inventory + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug == 'true' || github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'debug') }} + shell: bash + run: | + echo "Inventory path: ${{ steps.render_inventory.outputs.inventory_path }}" + echo + cat "${{ steps.render_inventory.outputs.inventory_path }}" + + - name: DEBUG libssh and pylibssh versions + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug == 'true' || github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'debug') }} + shell: bash + run: python -c "from pylibsshext import __full_version__; print(__full_version__)" + + - name: Run ansible-test network-integration - all + run: ansible-test network-integration --inventory "${{ steps.render_inventory.outputs.inventory_path }}" -vvvv + working-directory: ${{ steps.identify.outputs.collection_path }} + env: + LAB_NODES: ${{ needs.lab-create.outputs.lab_nodes_json }} + + lab-destroy: + if: ${{ always() }} + needs: integration-tests + uses: ansible/ansible-content-actions/.github/workflows/cml_lab_destroy.yaml@main + secrets: + virl_host: ${{ secrets.VIRL_HOST }} + virl_username: ${{ secrets.VIRL_USERNAME }} + virl_password: ${{ secrets.VIRL_PASSWORD }} diff --git a/collections/ansible_collections/ansible/netcommon/.github/workflows/release.yml b/collections/ansible_collections/ansible/netcommon/.github/workflows/release.yml new file mode 100644 index 0000000..d5daa15 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/.github/workflows/release.yml @@ -0,0 +1,14 @@ +--- +name: "Release collection 🚀" +on: # yamllint disable-line rule:truthy + release: + types: [published] + +jobs: + release: + uses: ansible/ansible-content-actions/.github/workflows/release.yaml@main + with: + environment: release + secrets: + ah_token: ${{ secrets.AH_TOKEN }} + ansible_galaxy_api_key: ${{ secrets.ANSIBLE_GALAXY_API_KEY }} diff --git a/collections/ansible_collections/ansible/netcommon/.github/workflows/tests.yml b/collections/ansible_collections/ansible/netcommon/.github/workflows/tests.yml new file mode 100644 index 0000000..858c3bb --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/.github/workflows/tests.yml @@ -0,0 +1,86 @@ +--- +name: "Collection Tests 🧪" + +concurrency: + group: ${{ github.head_ref || github.run_id }}-${{ github.event_name }} + cancel-in-progress: true + +on: # yamllint disable-line rule:truthy + push: + branches: [main] + pull_request_target: + branches: [main] + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + sonar: + name: SonarCloud + uses: ansible/ansible-content-actions/.github/workflows/sonarcloud.yaml@main + with: + python_version: "3.12" + galaxy_dependencies: | + git+https://github.com/ansible-collections/ansible.utils.git + test_command: "pip install passlib && pytest tests/unit -v --cov-report xml --cov=./" + secrets: + SONAR_TOKEN: ${{ secrets.ANSIBLE_COLLECTIONS_ORG_SONAR_TOKEN_CICD_BOT }} + changelog: + uses: ansible/ansible-content-actions/.github/workflows/changelog.yaml@main + if: github.event_name == 'pull_request_target' + build-import: + uses: ansible/ansible-content-actions/.github/workflows/build_import.yaml@main + ansible-lint: + uses: ansible/ansible-content-actions/.github/workflows/ansible_lint.yaml@main + sanity: + uses: ansible/ansible-content-actions/.github/workflows/sanity.yaml@main + unit-galaxy: + uses: ansible/ansible-content-actions/.github/workflows/unit.yaml@main + unit-source: + uses: ansible-network/github_actions/.github/workflows/unit_source.yml@main + with: + collection_pre_install: >- + git+https://github.com/ansible-collections/ansible.utils.git + + report-status: + if: ${{ always() && github.event_name == 'schedule' }} + needs: + - changelog + - build-import + - sanity + - unit-galaxy + - ansible-lint + - unit-source + uses: ansible/ansible-content-actions/.github/workflows/upload_upstream_results.yaml@main + with: + job_context: ${{ toJSON(needs) }} + component_name: ${{ github.event.repository.name }} + UPLOAD_USER: ${{ vars.PDE_ORG_RESULTS_AGGREGATOR_UPLOAD_USER }} + UPLOAD_URL: ${{ vars.PDE_ORG_RESULTS_AGGREGATOR_UPLOAD_URL }} + secrets: + UPLOAD_PASSWORD: ${{ secrets.PDE_ORG_RESULTS_UPLOAD_PASSWORD }} + + all_green: + if: ${{ always() }} + needs: + - changelog + - build-import + - sanity + - unit-galaxy + - unit-source + - ansible-lint + - sonar + - report-status + runs-on: ubuntu-latest + steps: + - run: >- + python -c "assert 'failure' not in + set([ + '${{ needs.changelog.result }}', + '${{ needs.sanity.result }}', + '${{ needs.unit-galaxy.result }}', + '${{ needs.ansible-lint.result }}', + '${{ needs.unit-source.result }}', + '${{ needs.sonar.result }}', + '${{ needs.report-status.result }}' + ])" diff --git a/collections/ansible_collections/ansible/netcommon/.github/workflows/token_refresh.yml b/collections/ansible_collections/ansible/netcommon/.github/workflows/token_refresh.yml new file mode 100644 index 0000000..8f6ee87 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/.github/workflows/token_refresh.yml @@ -0,0 +1,14 @@ +--- +name: "AH Token Refresh 🔧" +on: # yamllint disable-line rule:truthy + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + refresh: + uses: ansible/ansible-content-actions/.github/workflows/refresh_ah_token.yaml@main + with: + environment: release + secrets: + ah_token: ${{ secrets.AH_TOKEN }} diff --git a/collections/ansible_collections/ansible/netcommon/CHANGELOG.rst b/collections/ansible_collections/ansible/netcommon/CHANGELOG.rst new file mode 100644 index 0000000..93b669b --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/CHANGELOG.rst @@ -0,0 +1,839 @@ +========================================== +Ansible Netcommon Collection Release Notes +========================================== + +.. contents:: Topics + +v8.5.0 +====== + +Minor Changes +------------- + +- The dependency on ansible-pylibssh (for ssh_type=libssh / network_cli) is now ansible-pylibssh>=1.4.0 in requirements.txt, raised from the previous >=0.2.0 requirement. Installations still on ansible-pylibssh 0.x or 1.x below 1.4.0 must upgrade to use the libssh connection path with this collection release. +- libssh connection - When log_path is set (e.g. via ANSIBLE_LOG_PATH or log_path in ansible.cfg), the plugin now routes ansible-pylibssh (libssh) + logs into the same Ansible log file. Log level is derived from display.verbosity using Python standard logging: verbosity 0 -> WARNING, + 1-2 -> INFO, 3+ -> DEBUG. This allows SSH/libssh debug and trace output to appear in the configured log file for troubleshooting without changing + ansible-pylibssh configuration manually. +- network_cli - The in-collection paramiko path supports the same host key policy behavior (including host_key_auto_add and known_hosts handling) and persistent connection caching as the previous ansible-core paramiko integration. +- network_cli - When ssh_type is set to paramiko, the connection plugin now uses an in-collection paramiko implementation instead of loading ansible-core's paramiko connection plugin. This allows network_cli to work with versions of ansible-core, where the paramiko connection plugin was removed. + +Deprecated Features +------------------- + +- network_cli - The in-collection paramiko support (used when ssh_type is paramiko) is a compatibility layer for environments where ansible-core's paramiko connection is no longer available. This layer is deprecated and will be removed in a release after 2028-02-01. Migrate to ssh_type=libssh by installing the ansible-pylibssh package. + +Bugfixes +-------- + +- filter plugins - Add plugin_routing redirects for ``ipaddr``, ``ipv4``, and ``ipv6`` to ``ansible.utils`` so short names work when ``ansible.netcommon`` is in the play's collection list (https://github.com/ansible-collections/ansible.utils/issues/404). +- filter plugins - Convert filter arguments to native Python types before ``AnsibleArgSpecValidator`` so filters work with Ansible 2.19+ lazy containers that cannot be deep-copied (e.g. ``vlan_parser``, ``vlan_expander``, ``hash_salt``, ``type5_pw``, ``comp_type5``, ``parse_cli``, ``parse_cli_textfsm``, ``parse_xml``, ``pop_ace``). +- libssh connection - Fixed test_libssh_put_file unit test so the put_file code path (used by net_put, copy module, and other file transfer + over libssh) is properly tested in CI. The test now sets connection options and mocks Session so put_file does not trigger a real connection + attempt with an unset host (was failing with "Hostname required"). +- network action plugin - Fall back when remove_internal_keys is not importable from ansible.vars.clean (e.g. some ansible-core builds), so direct module execution still cleans module return data. +- network_cli - Fixed file transfer (net_put / net_get) when ssh_type=libssh. For put_file, no longer call + _connect_uncached() before delegating to the libssh connection the libssh plugin's put_file() calls _connect() internally. + For fetch_file, call _connect() then fetch_file() for libssh instead of _connect_uncached(), so connection caching and the + correct flow are used. Paramiko branch unchanged (still uses _connect_uncached() for scp/sftp). + +v8.4.0 +====== + +Release Summary +--------------- + +Re-released 8.3.1 with features added in the last release. + +Minor Changes +------------- + +- Option to use libssh as transport while using netconf, is added. +- The ssh-python module is needed, which will ensure libssh as transport for netconf operations. When use_libssh is enabled. + +v8.3.0 +====== + +Minor Changes +------------- + +- Option to use libssh as transport while using netconf, is added. +- The ssh-python module is needed, which will ensure libssh as transport for netconf operations. When use_libssh is enabled. + +v8.2.1 +====== + +Bugfixes +-------- + +- Adds backward compatibility of handling src attributes, functional consistency with ansible-core >= 2.19 +- Adds deprecation warning for the jinja2 processing functionality for src attributes, src attributes in collections would still support considering file path but they would not process template files directly once the functionality is deprecated. +- It is suggested to use ansible.builtin.template module to process templates and use the processed template path in src attributes. + +v8.2.0 +====== + +Minor Changes +------------- + +- Exposes new libssh option to configure key_exchange_algorithms. This requires ansible-pylibssh v1.3.0 or higher. + +Bugfixes +-------- + +- Added support for private key passphrase in libssh connection plugin, when using encrypted private keys specified by the C(ansible_private_key_file) attribute. +- Avoid legacy imports deprecated in ansible-core 2.20 (https://github.com/ansible-collections/ansible.netcommon/pull/720). +- Avoid merging module_defaults for all ansible.netcommon.grpc_* modules. +- Set libssh logging level to DEBUG when Ansible verbosity is greater than 3, to aid in troubleshooting connection issues. + +v8.1.0 +====== + +Minor Changes +------------- + +- Changes to supplement direct execution of Ansible module in validate_config(utils.py) and _patch_update_module(network.py) added. +- Override new 2.19.1+ AnsibleModule._record_module_result hook in network action plugin to bypass module result serialization when direct execution is enabled + +Bugfixes +-------- + +- Improved error handling in DirectExecutionModule._record_module_result method for better compatibility with core<=2.18 + +v8.0.1 +====== + +Bugfixes +-------- + +- (#633) Fixed typo in ansible.netcommon.telnet parameter crlf (was clrf by mistake) +- netconf - Adds check for netconf session_close RPC happens only if connection is alive. + +v8.0.0 +====== + +Release Summary +--------------- + +With this release, the minimum required version of `ansible-core` for this collection is `2.16.0`. The last version known to be compatible with `ansible-core` versions below `2.16` is v7.2.0. + +Major Changes +------------- + +- Bumping `requires_ansible` to `>=2.16.0`, since previous ansible-core versions are EoL now. + +v7.2.0 +====== + +Minor Changes +------------- + +- Exposes new libssh options to configure publickey_accepted_algorithms and hostkeys. This requires ansible-pylibssh v1.1.0 or higher. + +Deprecated Features +------------------- + +- Added deprecation warnings for the above plugins, displayed when running respective filter plugins. +- `parse_cli_textfsm` filter plugin is deprecated and will be removed in a future release after 2027-02-01. Use `ansible.utils.cli_parse` with the `ansible.utils.textfsm_parser` parser as a replacement. +- `parse_cli` filter plugin is deprecated and will be removed in a future release after 2027-02-01. Use `ansible.utils.cli_parse` as a replacement. +- `parse_xml` filter plugin is deprecated and will be removed in a future release after 2027-02-01. Use `ansible.utils.cli_parse` with the `ansible.utils.xml_parser` parser as a replacement. + +Bugfixes +-------- + +- libssh connection plugin - stop using long-deprecated and now removed internal field from ansible-core's base connection plugin class (https://github.com/ansible-collections/ansible.netcommon/issues/522, https://github.com/ansible-collections/ansible.netcommon/issues/690, https://github.com/ansible-collections/ansible.netcommon/pull/691). + +Documentation Changes +--------------------- + +- Includes a new support related section in the README. + +v7.1.0 +====== + +Minor Changes +------------- + +- ansible.netcommon.persistent - Connection local is marked deprecated and all dependent collections are advised to move to a proper connection plugin, complete support of connection local will be removed in a release after 01-01-2027. + +Bugfixes +-------- + +- Updated the error message for the content_templates parser to include the correct parser name and detailed error information. + +Documentation Changes +--------------------- + +- Add a simple regexp match example for multiple prompt with multiple answers. This example could be used to for restarting a network device with a delay. + +v7.0.0 +====== + +Release Summary +--------------- + +Starting from this release, the minimum `ansible-core` version this collection requires is `2.15.0`. The last known version compatible with ansible-core<2.15 is v6.1.3. + +Major Changes +------------- + +- Bumping `requires_ansible` to `>=2.15.0`, since previous ansible-core versions are EoL now. + +Bugfixes +-------- + +- Fix get api call during scp with libssh. +- Handle sftp error messages for file not present for routerOS. + +Known Issues +------------ + +- libssh - net_put and net_get fail when the destination file intended to be fetched is not present. + +v6.1.3 +====== + +Bugfixes +-------- + +- The v6.1.2 release introduced a change in cliconfbase's edit_config() signature which broke many platform cliconfs. This patch release reverts that change. + +v6.1.2 +====== + +Documentation Changes +--------------------- + +- Fixed module name and log consistency in parse_cli_textfsm filter doc. + +v6.1.1 +====== + +Bugfixes +-------- + +- Added guidance for users to open an issue for the respective platform if plugin support is needed. +- Improved module execution to gracefully handle cases where plugin support is required, providing a clear error message to the user. + +v6.1.0 +====== + +Minor Changes +------------- + +- Add new module cli_restore that exclusively handles restoring of backup configuration to target applaince. + +Bugfixes +-------- + +- libssh connection plugin - stop using deprecated ``PlayContext.verbosity`` property that is no longer present in ansible-core 2.18 (https://github.com/ansible-collections/ansible.netcommon/pull/626). +- network_cli - removed deprecated play_context.verbosity property. + +New Modules +----------- + +- cli_restore - Restore device configuration to network devices over network_cli + +v6.0.0 +====== + +Release Summary +--------------- + +Starting from this release, the minimum `ansible-core` version this collection requires is `2.14.0`. That last known version compatible with ansible-core<2.14 is `v5.3.0`. + +Major Changes +------------- + +- Bumping `requires_ansible` to `>=2.14.0`, since previous ansible-core versions are EoL now. + +v5.3.0 +====== + +Minor Changes +------------- + +- Add new module cli_backup that exclusively handles configuration backup. + +Bugfixes +-------- + +- Fix attribute types from string to str in filter plugins. + +v5.2.0 +====== + +Minor Changes +------------- + +- Add a new cliconf plugin ``default`` that can be used when no cliconf plugin is found for a given network_os. This plugin only supports ``get()``. (https://github.com/ansible-collections/ansible.netcommon/pull/569) +- httpapi - Add additional option ``ca_path``, ``client_cert``, ``client_key``, and ``http_agent`` that are available in open_url but not to httpapi. (https://github.com/ansible-collections/ansible.netcommon/issues/528) +- telnet - add crlf option to send CRLF instead of just LF (https://github.com/ansible-collections/ansible.netcommon/pull/440). + +Deprecated Features +------------------- + +- libssh - the ssh_*_args options are now marked that they will be removed after 2026-01-01. + +Bugfixes +-------- + +- Ensure that all connection plugin options that should be strings are actually strings (https://github.com/ansible-collections/ansible.netcommon/pull/549). + +New Plugins +----------- + +Cliconf +~~~~~~~ + +- default - General purpose cliconf plugin for new platforms + +v5.1.3 +====== + +Bugfixes +-------- + +- Vendor telnetlib from cpython (https://github.com/ansible-collections/ansible.netcommon/pull/546) + +v5.1.2 +====== + +Bugfixes +-------- + +- Ensure that all connection plugin options that should be strings are actually strings (https://github.com/ansible-collections/ansible.netcommon/pull/549). + +v5.1.1 +====== + +Bugfixes +-------- + +- network_resource - do not append network_os to module names when building supported resources list. This fix is only valid for cases where FACTS_RESOURCE_SUBSETS is undefined. + +v5.1.0 +====== + +Minor Changes +------------- + +- libssh - add ``config_file`` option to specify an alternate SSH config file to use. +- parse_cli - add support for multiple matches inside a block by adding new dictionary key to result +- telnet - add ``stdout`` and ``stdout_lines`` to module output. +- telnet - add support for regexes to ``login_prompt`` and ``password_prompt``. +- telnet - apply ``timeout`` to command prompts. + +Bugfixes +-------- + +- httpapi - ``send()`` method no longer applied leftover kwargs to ``open_url()``. Fix applies those arguments as intended (https://github.com/ansible-collections/ansible.netcommon/pull/524). +- network_cli - network cli connection avoids traceback when using invalid user +- network_cli - when receiving longer responses with libssh, parts of the response were sometimes repeated. The response is now returned as it is received (https://github.com/ansible-collections/community.routeros/issues/132). +- network_resource - fix a potential UnboundLocalError if the module fails to import a Resource Module. (https://github.com/ansible-collections/ansible.netcommon/pull/513) +- restconf - creation of new resources is no longer erroneously forced to use POST. (https://github.com/ansible-collections/ansible.netcommon/issues/502) + +v5.0.0 +====== + +Minor Changes +------------- + +- httpapi - Add option netcommon_httpapi_ciphers to allow overriding default SSL/TLS ciphers. (https://github.com/ansible-collections/ansible.netcommon/pull/494) + +Breaking Changes / Porting Guide +-------------------------------- + +- NetworkConnectionBase now inherits from PersistentConnectionBase in ansible.utils. As a result, the minimum ansible.utils version has increased to 2.7.0. +- NetworkTemplate is no longer importable from ansible_collections.ansible.netcommon.plugins.module_utils.network.common and should now be found at its proper location ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template +- ResourceModule is no longer importable from ansible_collections.ansible.netcommon.plugins.module_utils.network.common and should now be found at its proper location ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module +- VALID_MASKS, is_masklen, is_netmask, to_bits, to_ipv6_network, to_masklen, to_netmask, and to_subnet are no longer importable from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils and should now be found at their proper location ansible.module_utils.common.network + +Removed Features (previously deprecated) +---------------------------------------- + +- cli_parse - This plugin was moved to ansible.utils in version 1.0.0, and the redirect to that collection has now been removed. + +Bugfixes +-------- + +- Cast AnsibleUnsafeText to str in convert_doc_to_ansible_module_kwargs() to keep CSafeLoader happy. This fixes issues with content scaffolding tools. + +v4.1.0 +====== + +Minor Changes +------------- + +- Add implementation for content_templates_parser. + +Bugfixes +-------- + +- restconf_get - fix direction of XML deserialization when ``output == 'xml'`` + +v4.0.0 +====== + +Removed Features (previously deprecated) +---------------------------------------- + +- napalm - Removed unused connection plugin. +- net_banner - Use _banner instead. +- net_interface - Use _interfaces instead. +- net_l2_interface - Use _l2_interfaces instead. +- net_l3_interface - Use _l3_interfaces instead. +- net_linkagg - Use _lag_interfaces instead. +- net_lldp - Use _lldp_global instead. +- net_lldp_interface - Use _lldp_interfaces instead. +- net_logging - Use _logging_global instead. +- net_static_route - Use _static_routes instead. +- net_system - Use _system instead. +- net_user - Use _user instead. +- net_vlan - Use _vlans instead. +- net_vrf - Use _vrf instead. + +v3.1.3 +====== + +Release Summary +--------------- + +The v3.1.2 is unavailable on Ansible Automation Hub because a technical issue. Please download and use v3.1.3 from Automation Hub. + +v3.1.2 +====== + +Bugfixes +-------- + +- libssh - check for minimum ansible-pylibssh version before using password_prompt option. (https://github.com/ansible-collections/ansible.netcommon/pull/467) + +v3.1.1 +====== + +Bugfixes +-------- + +- Fix a small number of potential use-before-assignment issues. +- Fix to set connection plugin options correctly. +- libssh - Removed the wording "Tech preview". From version 3.0.0 the default if installed. +- libssh - add ssh_args, ssh_common_args, and ssh_extra_args options. These options are exclusively for collecting proxy information from as an alternative to the proxy_command option. + +v3.1.0 +====== + +Minor Changes +------------- + +- Add grpc connection plugin support. +- Adds a new option `terminal_errors` in network_cli, that determines how terminal setting failures are handled. +- libssh - Added `password_prompt` option to override default "password:" prompt used by pylibssh + +New Plugins +----------- + +Connection +~~~~~~~~~~ + +- grpc - Provides a persistent connection using the gRPC protocol + +New Modules +----------- + +- grpc_config - Fetch configuration/state data from gRPC enabled target hosts. +- grpc_get - Fetch configuration/state data from gRPC enabled target hosts. + +v3.0.1 +====== + +Bugfixes +-------- + +- httpapi - Fix for improperly set hostname in url +- libssh - Fix for improperly set hostname in connect +- restconf - When non-JSON data is encountered, return the bytes found instead of nothing. + +v3.0.0 +====== + +Major Changes +------------- + +- cli_parse - this module has been moved to the ansible.utils collection. ``ansible.netcommon.cli_parse`` will continue to work to reference the module in its new location, but this redirect will be removed in a future release +- network_cli - Change default value of `ssh_type` option from `paramiko` to `auto`. This value will use libssh if the ansible-pylibssh module is installed, otherwise will fallback to paramiko. + +Breaking Changes / Porting Guide +-------------------------------- + +- httpapi - Change default value of ``import_modules`` option from ``no`` to ``yes`` +- netconf - Change default value of ``import_modules`` option from ``no`` to ``yes`` +- network_cli - Change default value of ``import_modules`` option from ``no`` to ``yes`` + +Known Issues +------------ + +- eos - When using eos modules on Ansible 2.9, tasks will occasionally fail with ``import_modules`` enabled. This can be avoided by setting ``import_modules: no`` + +v2.6.1 +====== + +Release Summary +--------------- + +Rereleased 2.6.0 with updated utils dependancy. + +Bugfixes +-------- + +- Fix validate-module sanity test. + +v2.6.0 +====== + +Minor Changes +------------- + +- Redirected ipaddr filters to ansible.utils (https://github.com/ansible-collections/ansible.netcommon/pull/359). +- httpapi - new parameter retries in send() method limits the number of times a request is retried when a HTTP error that can be worked around is encountered. The default is to retry indefinitely to maintain old behavior, but this default may change in a later breaking release. + +Bugfixes +-------- + +- Fix issue with cli_parse native_parser plugin when input is empty (https://github.com/ansible-collections/ansible.netcommon/issues/347). +- No activity on the transport's channel was triggering a socket.timeout() after 30 secs, even if persistent_command_timeout is set to a higher value. This patch fixes it. + +v2.5.1 +====== + +Bugfixes +-------- + +- Fixed plugins inheriting from netcommon's base plugins (for example httpapi/restconf or netconf/default) so that they can be properly loaded (https://github.com/ansible-collections/ansible.netcommon/issues/356). + +v2.5.0 +====== + +Minor Changes +------------- + +- Copied the cliconf, httpapi, netconf, and terminal base plugins and NetworkConnectionBase into netcommon. These base plugins may now be imported from netcommmon instead of ansible if a collection depends on netcommon versions newer than this version, allowing features and bugfixes to flow to those collections without upgrading ansible. +- Make ansible_network_os as optional param for httpapi connection plugin. +- Support removal of non-config lines from running config while taking backup. +- `network_cli` - added new option 'become_errors' to determine how privilege escalation failures are handled. + +Bugfixes +-------- + +- network_cli - Provide clearer error message when a prompt regex fails to compile +- network_cli - fix issue when multiple terminal_initial_(prompt|answer) values are given (https://github.com/ansible-collections/ansible.netcommon/issues/331). + +v2.4.0 +====== + +Minor Changes +------------- + +- Add network_resource plugin to manage and provide single entry point for all resource modules for higher oder roles. + +Deprecated Features +------------------- + +- network_cli - The paramiko_ssh setting ``look_for_keys`` was set automatically based on the values of the ``password`` and ``private_key_file`` options passed to network_cli. This option can now be set explicitly, and the automatic setting of ``look_for_keys`` will be removed after 2024-01-01 (https://github.com/ansible-collections/ansible.netcommon/pull/271). + +Bugfixes +-------- + +- network_cli - Add ability to set options inherited from paramiko/libssh in ansible >= 2.11 (https://github.com/ansible-collections/ansible.netcommon/pull/271). + +New Modules +----------- + +- network_resource - Manage resource modules + +v2.3.0 +====== + +Minor Changes +------------- + +- Add vlan_expander filter +- Persistent connection options (persistent_command_timeout, persistent_log_messages, etc.) have been unified across all persistent connections. New persistent connections may also now get these options by extending the connection_persistent documentation fragment. + +v2.2.0 +====== + +Minor Changes +------------- + +- Add variable to control ProxyCommand with libssh connection. +- NetworkTemplate and ResouceModule base classes have been moved under module_utils.network.common.rm_base. Stubs have been kept for backwards compatibility. These will be removed after 2023-01-01. Please update imports for existing modules that subclass them. The `cli_rm_builder `_ has been updated to use the new imports. + +Bugfixes +-------- + +- libssh - Fix fromatting of authenticity error message when not prompting for input (https://github.com/ansible-collections/ansible.netcommon/issues/283) +- netconf - Fix connection with ncclient versions < 0.6.10 +- network_cli - Fix for execution failing when ansible_ssh_password is used to specify password (https://github.com/ansible-collections/ansible.netcommon/issues/288) + +v2.1.0 +====== + +Minor Changes +------------- + +- Add support for ProxyCommand with netconf connection. + +Bugfixes +-------- + +- Variables in play_context will now be updated for netconf connections on each task run. +- fix SCP/SFTP when using network_cli with libssh + +v2.0.2 +====== + +Bugfixes +-------- + +- Fix cli_parse issue with parsers in utils collection (https://github.com/ansible-collections/ansible.netcommon/pull/270) +- Support single_user_mode with Ansible 2.9. + +v2.0.1 +====== + +Minor Changes +------------- + +- Several module_utils files were intended to be licensed BSD, but missing a license preamble in the files. The preamble has been added, and all authors for the files have given their assent to the intended license https://github.com/ansible-collections/ansible.netcommon/pull/122 + +Bugfixes +-------- + +- Allow setting `host_key_checking` through a play/task var for `network_cli`. +- Ensure passed-in terminal_initial_prompt and terminal_initial_answer values are cast to bytes before using +- Update valid documentation for net_ping module. +- ncclient - catch and handle exception to prevent stack trace when running in FIPS mode +- net_put - Remove temp file created when file already exist on destination when mode is 'text'. + +v2.0.0 +====== + +Major Changes +------------- + +- Remove deprecated connection arguments from netconf_config + +Minor Changes +------------- + +- Add SCP support when using ssh_type libssh +- Add `single_user_mode` option for command output caching. +- Move cli_config idempotent warning message with the task response under `warnings` key if `changed` is `True` +- Reduce CPU usage and network module run time when using `ansible_network_import_modules` +- Support any() and all() filters in Jinja2. + +Breaking Changes / Porting Guide +-------------------------------- + +- Removed vendored ipaddress package from collection. If you use ansible_collections.ansible.netcommon.plugins.module_utils.compat.ipaddress in your collection, you will need to change this to import ipaddress instead. If your content using ipaddress supports Python 2.7, you will additionally need to make sure that the user has the ipaddress package installed. Please refer to https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_best_practices.html#importing-and-using-shared-code to see how to safely import external packages that may be missing from the user's system A backport of ipaddress for Python 2.7 is available at https://pypi.org/project/ipaddress/ + +Deprecated Features +------------------- + +- Deprecate cli_parse module and textfsm, ttp, xml, json parser plugins as they are moved to ansible.utils collection (https://github.com/ansible-collections/ansible.netcommon/pull/182 https://github.com/ansible-collections/ansible.utils/pull/28) + +Bugfixes +-------- + +- Expose connection class object to rm_template (https://github.com/ansible-collections/ansible.netcommon/pull/180) +- network_cli - When using ssh_type libssh, handle closed connection gracefully instead of throwing an exception + +New Plugins +----------- + +Cache +~~~~~ + +- memory - RAM backed, non persistent cache. + +v1.5.0 +====== + +Minor Changes +------------- + +- Add 'purged' to ACTION_STATES. + +Bugfixes +-------- + +- Add netconf_config integration tests for nxos (https://github.com/ansible-collections/ansible.netcommon/pull/185) +- Fix GetReply object has no attribute strip() (https://github.com/ansible-collections/cisco.iosxr/issues/97) +- Fix config diff logic if parent configuration is present more than once in the candidate config and update docs (https://github.com/ansible-collections/ansible.netcommon/pull/189) +- Fix missing changed from net_get (https://github.com/ansible-collections/ansible.netcommon/issues/198) +- Fix netconf_config module integration test issuea (https://github.com/ansible-collections/ansible.netcommon/pull/177) +- Fix restconf_config incorrectly spoofs HTTP 409 codes (https://github.com/ansible-collections/ansible.netcommon/issues/191) +- Split checks for prompt and errors in network_cli so that detected errors are not lost if the prompt is in a later chunk. + +v1.4.1 +====== + +Release Summary +--------------- + +Change how black config is specified to avoid issues with Automation Hub release process + +v1.4.0 +====== + +Minor Changes +------------- + +- 'prefix' added to NetworkTemplate class, inorder to handle the negate operation for vyos config commands. +- Add support for json format input format for netconf modules using ``xmltodict`` +- Update docs for netconf_get and netconf_config examples using display=native + +Bugfixes +-------- + +- Added support for private key based authentication with libssh transport (https://github.com/ansible-collections/ansible.netcommon/issues/168) +- Fixed ipaddr filter plugins in ansible.netcommon collections is not working with latest Ansible (https://github.com/ansible-collections/ansible.netcommon/issues/157) +- Fixed netconf_rpc task fails due to encoding issue in the response (https://github.com/ansible-collections/ansible.netcommon/issues/151) +- Fixed ssh_type none issue while using net_put and net_get module (https://github.com/ansible-collections/ansible.netcommon/issues/153) +- Fixed unit tests under python3.5 +- ipaddr filter - query "address/prefix" (also: "gateway", "gw", "host/prefix", "hostnet", and "router") now handles addresses with /32 prefix or /255.255.255.255 netmask +- network_cli - Update underlying ssh connection's play_context in update_play_context, so that the username or password can be updated + +v1.3.0 +====== + +Minor Changes +------------- + +- Confirmed commit fails with TypeError in IOS XR netconf plugin (https://github.com/ansible-collections/cisco.iosxr/issues/74) +- The netconf_config module now allows root tag with namespace prefix. +- cli_config: Add new return value diff which is returned when the cliconf plugin supports onbox diff +- cli_config: Clarify when commands is returned when the module is run + +Bugfixes +-------- + +- cli_parse - Ensure only native types are returned to the control node from the parser. +- netconf - Changed log level for message of using default netconf plugin to match the level used when a platform-specific netconf plugin is found + +v1.2.1 +====== + +Bugfixes +-------- + +- Fixed "Object of type Capabilities is not JSON serializable" when using default netconf plugin. + +v1.2.0 +====== + +Minor Changes +------------- + +- Added description to collection galaxy.yml file. +- NetworkConfig objects now have an optional `comment_tokens` parameter which takes a list of strings which will override the DEFAULT_COMMENT_TOKENS list. +- New cli_parse module for parsing structured text using a variety of parsers. The initial implemetation of cli_parse can be used with json, native, ntc_templates, pyats, textfsm, ttp, and xml. +- The httpapi connection plugin now works with `wait_for_connection`. This will periodically request the root page of the server described by the plugin's options until the request succeeds. This can only test that the server is reachable, the correctness or usability of the API is not guaranteed. + +Bugfixes +-------- + +- cli_config fixes issue when rollback_id = 0 evalutes to False +- sort_list will sort a list of dicts using the sorted method with key as an argument. + +v1.1.2 +====== + +Release Summary +--------------- + +Rereleased 1.1.1 with updated changelog. + +v1.1.1 +====== + +Release Summary +--------------- + +Rereleased 1.1.0 with regenerated documentation. + +v1.1.0 +====== + +Major Changes +------------- + +- Add libssh connection plugin and refactor network_cli (https://github.com/ansible-collections/ansible.netcommon/pull/30) + +Minor Changes +------------- + +- Add content option validation for netconf_config module (https://github.com/ansible-collections/ansible.netcommon/pull/66) +- Documentation of module arguments updated to match expected types where missing. +- Resource Modules: changed flag is set to true in check_mode for all ACTION_STATES (https://github.com/ansible-collections/ansible.netcommon/pull/82) + +Removed Features (previously deprecated) +---------------------------------------- + +- module_utils.network.common.utils.ComplexDict has been removed + +Bugfixes +-------- + +- Replace deprecated `getiterator` call with `iter` +- ipaddr - "host" query supports /31 subnets properly +- ipaddr filter - Fixed issue where the first IPv6 address in a subnet was not being considered a valid address. +- ipaddr filter now returns empty list instead of False on empty list input +- net_put - Restore missing function removed when action plugin stopped inheriting NetworkActionBase +- nthhost filter now returns str instead of IPAddress object +- slaac filter now returns str instead of IPAddress object + +v1.0.0 +====== + +New Plugins +----------- + +Become +~~~~~~ + +- enable - Switch to elevated permissions on a network device + +Connection +~~~~~~~~~~ + +- httpapi - Use httpapi to run command on network appliances +- netconf - Provides a persistent connection using the netconf protocol +- network_cli - Use network_cli to run command on network appliances +- persistent - Use a persistent unix socket for connection + +Httpapi +~~~~~~~ + +- restconf - HttpApi Plugin for devices supporting Restconf API + +Netconf +~~~~~~~ + +- default - Use default netconf plugin to run standard netconf commands as per RFC + +New Modules +----------- + +- cli_command - Run a cli command on cli-based network devices +- cli_config - Push text based configuration to network devices over network_cli +- net_get - Copy a file from a network device to Ansible Controller +- net_ping - Tests reachability using ping from a network device +- net_put - Copy a file from Ansible Controller to a network device +- netconf_config - netconf device configuration +- netconf_get - Fetch configuration/state data from NETCONF enabled network devices. +- netconf_rpc - Execute operations on NETCONF enabled network devices. +- restconf_config - Handles create, update, read and delete of configuration data on RESTCONF enabled devices. +- restconf_get - Fetch configuration/state data from RESTCONF enabled devices. +- telnet - Executes a low-down and dirty telnet command diff --git a/collections/ansible_collections/ansible/netcommon/FILES.json b/collections/ansible_collections/ansible/netcommon/FILES.json new file mode 100644 index 0000000..ca840ac --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/FILES.json @@ -0,0 +1,2826 @@ +{ + "files": [ + { + "name": ".", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "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/config.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5fc940cdae49577df5bc9b35b1f0bbbe531e9dd75569c60c636c39ea8b608aa4", + "format": 1 + }, + { + "name": "changelogs/changelog.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "40dc65e1ba18130326bc850c3faf3dd4922957523c26d40e744deb2c423fc471", + "format": 1 + }, + { + "name": "changelogs/CHANGELOG.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0ccda3a775ac79df07a7a7f0cff6c9f157793d95e60f5fe4a2326592b346819e", + "format": 1 + }, + { + "name": "README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adcd5f1b7c51fd96478bce22abf3b0ea9be4d36b136ed0c5ec59bcd169e6c262", + "format": 1 + }, + { + "name": ".github", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/release-drafter.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7cbc785c8661033cd9dd7a60897484fce731cbe3dc124a689fc3e934b1d974fb", + "format": 1 + }, + { + "name": ".github/workflows", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": ".github/workflows/tests.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f9d8cf16476a2d66fb42a81ab3c499d80d07da2547696f1f3759ce7757c88249", + "format": 1 + }, + { + "name": ".github/workflows/draft_release.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7c39a205f2120c2663149f9ca088093dc72e7091e7c9cc0f4c460895ced48494", + "format": 1 + }, + { + "name": ".github/workflows/token_refresh.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f02f72dbcbb0d2d1c723c923f1378219a5a1521249c0fbaab891e417619cb3a9", + "format": 1 + }, + { + "name": ".github/workflows/integration-cml.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8a24590a49e59292259121490c52db8c0455891332c6f653f3a86de8f9a451e2", + "format": 1 + }, + { + "name": ".github/workflows/release.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e2a5309e138b18586e95ebb23c38922b50edc1e87298c757f3d7576912235b24", + "format": 1 + }, + { + "name": ".github/workflows/check_label.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f1aceb4971acbf14a88de6304079f7e93aebab7b91ec320a186a92f564cbf5ce", + "format": 1 + }, + { + "name": "tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/config.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "71ff5426485cc74b3c360a0941dc40661ef17ffc8983435481f55f9f4143ae4f", + "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/prepare_nxos_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_nxos_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_nxos_tests/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f2b802dd1945d48cc7bca206d731222406689dc4282958ae580a51144aa25250", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/tests/iosxr", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/tests/iosxr/basic.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9123cf26967226d5fd990be9334afea5a7b088ce27e7224abe20fdacd81486f8", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f393b4929a03985a5dbb1a5b15bee46ec2b84b0839313a6ea4b107749ce9a325", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/tasks/iosxr.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f22ef7b541fd9c3abbc306cab3651afc018841dabd294dd2e71ad83fe296e46b", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d25d95206718d482774753d23b1563530440c6c3c76e2b40afd18ad95595cddd", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_rpc/defaults/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b6cec8117492a3110c2e9066aa77a54abd2b9774cea08d60eb42b01c51c3e032", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/tests/iosxe", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/tests/iosxe/basic.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5966ad1dd1484d3e64e47e715605e6227d1373a4f338a93df6c0f964a4917957", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "344d9c0ea63d8d064ef8a6c738551c1f1b48e68e26e5ccb64add0de511dc58c1", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/tasks/restconf.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "02bb9df13439458227da76f8f5ad2165ea1a6556aafa17842ea8b4f3ab10497f", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f217b1000606ebe64f449135a62b62813f672461ce258e4168b48ef56fdf7b55", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_get/defaults/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b6cec8117492a3110c2e9066aa77a54abd2b9774cea08d60eb42b01c51c3e032", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tests/nxos", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tests/nxos/fixtures", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tests/nxos/fixtures/config.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "094f18d3ff543306a3c3d28eb6ca6851687f5c38b4e50e6021e93e89b8518587", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tests/nxos/basic.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3d5e05ea2718e82441fba21c106a2c105aba6c7a0549808b8b112fe33de6ebbe", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tests/iosxr", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tests/iosxr/basic.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "132ebedce4076e5c27d8601900e92b45de514c85ee0ff769136dcb5d4aa95620", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a52bdff964abb40816a0c8e5a4edf85383755677d24e55065b1eea525d1516e6", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tasks/iosxr.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "94c8ebaa442d7f1e6c23a6c67c1aa2436d3b706613826c72eb5f45aed5947e71", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/tasks/nxos.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "00b6114a67d3d51d946878332693517f386bb1f98a2f910ecd65b0a02a5a8a1b", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d25d95206718d482774753d23b1563530440c6c3c76e2b40afd18ad95595cddd", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_config/defaults/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b6cec8117492a3110c2e9066aa77a54abd2b9774cea08d60eb42b01c51c3e032", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/tasks/nxos_pyats.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6d3dbab19620dc96a3c99e11f7c6284f6e40c5e8899ff2c01ab06682678e5bac", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e4063a3eb4c5572fc768ec542980aeb9f1c75dc7441809ef6c5f8e0517ad01b1", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/tasks/ubuntu_native.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "095d4826ffe18c592c5bd1a346a8d82c4fa555b20c3b28ca4da49193dac53e19", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/tasks/nxos_textfsm.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8a72d08d7ea2564f02506aa0d06bb055ba9e6a24e122055931391e6bec3bf900", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/tasks/nxos_xml.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4152f6872860af99cd16db131356fc51988e9e669950fa14ea2a01e6d8c15246", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/tasks/nxos_native.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7336cd006b8234f06e4602c67e629073a9ac02433fcbb8419c23d6afc453e6c4", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/tasks/nxos_ntc_templates.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c2c3716c089f2193799349a1af68e3b07bb9dfc8c7251bce348f629c309b7cd3", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/templates/nxos_show_interface.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bcebc3ef1d3e94fd156e655704999ee16837a1c95b90c6d8bbd567e47e4ce272", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/templates/ubuntu_ps_-ef.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9ea8dada061be6fe0f6d37e41b2139d9f4c9471c85a761cafdc16373911dc2b4", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/templates/ubuntu_iptables_--list.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "41f749e88e4d6a959350d34a9e887112d76e3f14a5bc15f2ce0485c2b17c7410", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/templates/nxos_show_interface.ttp", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7b54c727fb9ab5731dbef7b5c271a41276278b15b7408bd24e25eda28e5c1a09", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/templates/nxos_parse_xml.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c1eba284ba3f2b2eb3860d1bb200d84bf70346566277eff342440f5bb3b01f5d", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/templates/nxos_show_version.textfsm", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dfd59adde11de6285a62179e45db7b4e20e9ca61f276f6567c86c8455f796887", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/fixtures", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cli_parse/fixtures/output.xml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1724699996695ecfae8b1c14b84238d9a6b05675ec730a858c925fb22272f52b", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cf314e5317e89f51519da01d3cc3c26a88bf532aaba4c2f2d6fd3e1ffc319ec9", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/tasks/nxos_cli_command.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ed85d12ba3bb6b6a8f3720cf2d48ee78625717d65a761349ad3a1a7b9706cd8", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/tasks/nxos_net_put.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "918742626407da091ec595d235b9da5e63d20ccc6d290123f73de29b523b2ae9", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/tasks/nxos_template_file.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6f65b9700328e73e69b87bfcb80ffff8cf8398ae9aea859a6e8667d9a31a3274", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/tasks/nxos_cli_config.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ea75df3f21840a958d837190d56f9bd947972636a97876724991c3e112778eb7", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/tasks/nxos_any_rm.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eebe8890856200caab6ec1c3e89547a1ce6b73a71f8e3c0dafbd0af35f6ed2b6", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/templates/basic", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/templates/basic/config_src_not_recommended.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "38116248be557a791937550a66d887cfa64dae7846a3b4a31a784bae682386e4", + "format": 1 + }, + { + "name": "tests/integration/targets/cli_tests/templates/basic/config_src_recommended.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3906307e739e97bc7497721988082e2d5fc0b39ab07b18d176d40297c8243d97", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/tests/iosxr", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/tests/iosxr/basic.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "49471f18ebb18c0a682253595bfb71239ed4eb51b0a494c2aae83a3b87b65ea2", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "80b7793d492311f9e80729124fb268998dcce7f9f48cc9aefa7874fec69cacf9", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/tasks/iosxr.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "94c8ebaa442d7f1e6c23a6c67c1aa2436d3b706613826c72eb5f45aed5947e71", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d25d95206718d482774753d23b1563530440c6c3c76e2b40afd18ad95595cddd", + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/netconf_get/defaults/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b6cec8117492a3110c2e9066aa77a54abd2b9774cea08d60eb42b01c51c3e032", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxr_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxr_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxr_tests/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d8e0b28dd1c574dd3a8ce058ab18cdac7bca9ee404f686732c07992d1776d01e", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxr_tests/templates", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxr_tests/templates/config_grpc.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4ab4bbfb41335abe51b4f0d03087c1df2ccb9bac7d0097685a28659113d1b9fd", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxr_tests/templates/config.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a21d7b66f513ecb424fd6e23df9f0737c855281f3a5d0be6c856a0d2ce7af23b", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/tests/iosxe", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/tests/iosxe/basic.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ff4b6188157975c3ba7b396443e415b264965d11029b6a7972e6927688458172", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "83d68cb2489c764efacfccd9c7c3983f7fdb9727a74e216c12e445299ae5b915", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/tasks/restconf.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "02bb9df13439458227da76f8f5ad2165ea1a6556aafa17842ea8b4f3ab10497f", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f217b1000606ebe64f449135a62b62813f672461ce258e4168b48ef56fdf7b55", + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/restconf_config/defaults/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b6cec8117492a3110c2e9066aa77a54abd2b9774cea08d60eb42b01c51c3e032", + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxe_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxe_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/prepare_iosxe_tests/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b8848efe6ae5aed08e186ecab8d147bdafb90234ad7f8dd717d22562e81e2647", + "format": 1 + }, + { + "name": "tests/integration/targets/httpapi_tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/httpapi_tests/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/httpapi_tests/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f455b1937be572b44e1e137bc8befca6f99c5728e1e33d68c844817bf428946b", + "format": 1 + }, + { + "name": "tests/integration/targets/httpapi_tests/tasks/nxos_cli_command.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cc433d2b33f076280e73f3d8477a5b2dc8bd12da1e92f8d11bfcdd92627a933e", + "format": 1 + }, + { + "name": "tests/integration/targets/httpapi_tests/tasks/nxos_cli_config.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4c6dac83eedbe3d7dba59aa17be959d5ac780e577f4751c777c47c1c752b0a94", + "format": 1 + }, + { + "name": "tests/integration/targets/httpapi_tests/tasks/nxos_any_rm.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9f9e9f26230469f9c13484f11b696d66482a2aa9a14e0d7c56abb4bc4193d085", + "format": 1 + }, + { + "name": "tests/integration/target-prefixes.network", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3102fd9c157293f9a3e52cbf9c5f259cb8ec27cdc28f08e6d7e15ca9b139764a", + "format": 1 + }, + { + "name": "tests/integration/hold", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tests/iosxr", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tests/iosxr/fixtures", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tests/iosxr/fixtures/bgp_start.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "688fa378b5dbe6c30e1063e4466ff407d07d60cf7c5f9c55e3d501d3b2d16751", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tests/iosxr/fixtures/bgp_delete.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3ba3ba7b0dab60faee2bd21b4ce7401cf416901ec983ffa28a3956f4575a050", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tests/iosxr/basic.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c426561da31a6fd7d92a39b0fe9fd65b79ad46fbe8286e727982bc4088e818e5", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8490b1578816963939834794088f0d3b3d9fb394957ce490908d51365610aefe", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tasks/junos.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3dd46e3d52b0c0d7ce821dce9746174cb36becdb1413e6f40d87c084b52904c", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tasks/iosxr.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "76062c3d0aefb3e1566be3449cff1f8fef8851fcd572096ee5be24f4ca6c6799", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/tasks/nxos.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "359b3a59d9b54ab17952380716ded26e67264e5d11f3ff8ef56ad204f2970717", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d25d95206718d482774753d23b1563530440c6c3c76e2b40afd18ad95595cddd", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/defaults/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b6cec8117492a3110c2e9066aa77a54abd2b9774cea08d60eb42b01c51c3e032", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/vars", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_config/vars/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a50f43b84486b9cff7f927e79c55df0b65ebd7e8caa47259cf997d14bbbca600", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/tests/iosxr", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/tests/iosxr/basic.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3796d7e1a25ee4aa3fd7850e67da8c8fb8a6c0477d1fb494672ed261ad51c81", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/tasks/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "14aa6f229d2851f12ba6d10cce5e4b985e9ac66bf758ee656f52a6b90c655f2b", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/tasks/iosxr.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "76062c3d0aefb3e1566be3449cff1f8fef8851fcd572096ee5be24f4ca6c6799", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/meta/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d25d95206718d482774753d23b1563530440c6c3c76e2b40afd18ad95595cddd", + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/hold/grpc_get/defaults/main.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b6cec8117492a3110c2e9066aa77a54abd2b9774cea08d60eb42b01c51c3e032", + "format": 1 + }, + { + "name": "tests/integration/labs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/labs/multi.yaml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "af8bb16dba3fb21f1a9750f64f206eecea446c305c52f2856f4d2f6f0818ce74", + "format": 1 + }, + { + "name": "tests/integration/labs/inventory.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "05351c70632fba8936d33ee4a8842ac7fc72ca21aa775b5ee247e10b16c21c77", + "format": 1 + }, + { + "name": "tests/unit", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f055fdf90a1c9a5d3d2eb9068d4e45e5405dce7834323bcad18791376b95a9bb", + "format": 1 + }, + { + "name": "tests/unit/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/module_utils/network", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/module_utils/network/common", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/module_utils/network/common/test_utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d400a77ce4f49dd40d06f13970468e09583f0dda6f8e4166ffea7e25ef2a97e4", + "format": 1 + }, + { + "name": "tests/unit/module_utils/network/common/test_parsing.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e0c6e12f7d3207990c54fc6cafde27a57ace7e25050e9f9571757bd8b0593b0d", + "format": 1 + }, + { + "name": "tests/unit/module_utils/network/common/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/module_utils/network/common/test_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "47e508ce3db5f9dc98a587eee6a6cd79ad7a1f11bbe64d12ab561fe7a9b557ef", + "format": 1 + }, + { + "name": "tests/unit/module_utils/network/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/module_utils/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/cliconf", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/cliconf/test_default.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8fc4158d1269179c0c5eb1055d790f287583cf2b6e451087e2572c3b4c10bde8", + "format": 1 + }, + { + "name": "tests/unit/plugins/connection", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/connection/test_libssh.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f6a566f5af52174df21214b4e3d24568b23741a673569593708d80ced7791700", + "format": 1 + }, + { + "name": "tests/unit/plugins/connection/test_network_cli.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c135240a25b2e6ee8139ee5f61844ac4aa2052049a6882586d91c251e6a18c3a", + "format": 1 + }, + { + "name": "tests/unit/plugins/connection/test_netconf.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9febd68713e9c7e12f30a75304dc561f72b28641ff8f760b89f773d54413c25e", + "format": 1 + }, + { + "name": "tests/unit/plugins/connection/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins/action", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/action/test_net_put.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8985822dfaf87b2ed9c241c875dda5f2a0be4d57af6f834dbf30814592db3f0f", + "format": 1 + }, + { + "name": "tests/unit/plugins/action/network", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/action/network/test_network.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fda8e64fc97f2922497186a398bd9dfeffc606f17a6849cfc3618d0a302ac939", + "format": 1 + }, + { + "name": "tests/unit/plugins/plugin_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/plugin_utils/test_argspec_filter_utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3bef9a51146e0bbd30aecd6aee24519253b03d1aa696ef065828b8fc9863b8dc", + "format": 1 + }, + { + "name": "tests/unit/plugins/plugin_utils/test_version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2bd3627d999c0ee500821e176a323e3094e0f3f1dab4c1891b45ce170f6a0fe0", + "format": 1 + }, + { + "name": "tests/unit/plugins/cli_parsers", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/cli_parsers/test_pyats_parser.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2ddef4125ac7a07a22adb793debd36cb74184f4ba08767e84014723c50570c8f", + "format": 1 + }, + { + "name": "tests/unit/plugins/cli_parsers/fixtures", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/cli_parsers/fixtures/nxos_show_version.cfg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "280b9a49752bd9ebddf26798b7df94115c3f3402877d350e60b3236a33fb3f68", + "format": 1 + }, + { + "name": "tests/unit/plugins/cli_parsers/fixtures/ios_show_ip_interface_brief.cfg", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cf7860ac3fe8175f2a062881ecc52f977f14fdcf92ae1a60df68edda493f645a", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/test_vlan_parser.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f3e0a3296b315406a039a75f98550368609e426406162bf8775f19ce6581501b", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/test_vlan_extender.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "143706f65fb7ad05951504329deb05c676f42d4b79de1e7ce901005407da1d56", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/test_pop_ace.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1e75a321883ae2e3d208bf408658154aaa14d5d8b2fdb59b5588772c2d45a760", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/fixtures", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/fixtures/network", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/fixtures/network/show_vlans_xml_spec.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "155ed3b225b5bb5312d3b4551e6cd46f8c00171b45a1102f95b97b9312d5f647", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/fixtures/network/show_vlans_xml_single_value_spec.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "806a7c949715a66e5337e7b9755395d9534a348646412bdc22fc0544587e33bf", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/fixtures/network/show_vlans_xml_with_condition_spec.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d6485e4874c8c58d4ce1bd7fc4e055a8de0c169392494971976dfee8a4fb4ded", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/fixtures/network/show_vlans_xml_output.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c7ed33a3859e92f8bd04a8e07f3c782200b68e07aaa0e4ec3321bf8ef13a01dc", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/fixtures/network/show_vlans_xml_with_key_spec.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "62102cf4c0ae9f7e57bca7999c52257bfe6aff62d1e24153e5f4da9efac8dab3", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/test_network.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "afc60994d4972a7a119fdd2c86e20fe50f7dcdd18b22e089c35552da6cc256f3", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/comp_type5.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "12f49643b520c283ed1028739b30d4b251e3d07bbe368df09f4c1632e201a7eb", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/test_filter_convert_to_native.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "98703f7f7ad09fec691b99a366328845464e42cc66e0ce516444422298bb30cd", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/test_type5_pw.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1374a5a09fb3597ab33e3a193cf7b6209c0a8a21f483001fae5d475944fee431", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/test_hash_salt.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "64323bf1bd60940ab3565edc5de0a807bf8b8ba0a0cfebceffe88f03dd2cf621", + "format": 1 + }, + { + "name": "tests/unit/plugins/filter/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/mock", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/mock/yaml_helper.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d24247a2f4c568cb1a33c4347344be44abb38c027e13132fbd2e930854897930", + "format": 1 + }, + { + "name": "tests/unit/mock/loader.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "62b34770bc51c9af0cee87370b448c8c8afb033ce2e704d6d2288743db916714", + "format": 1 + }, + { + "name": "tests/unit/mock/vault_helper.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3bf7834d18bd34473d0f4b898253177229a131f14364874efba584ff985e4a41", + "format": 1 + }, + { + "name": "tests/unit/mock/path.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5561a16a302e55ea79227591afefc3a93f33001fc80155dd29b068b90502a446", + "format": 1 + }, + { + "name": "tests/unit/mock/procenv.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6e8080ee46c6b7aead9fc6a110e56ec5956b1cd6ea057b10bf23d2d64881a1cb", + "format": 1 + }, + { + "name": "tests/unit/mock/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/modules/utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0653ed8cbe03a205db135057dba4586f688c7e2666e106dcfd32cd03d88f2cec", + "format": 1 + }, + { + "name": "tests/unit/modules/network", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/modules/network/cli", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/modules/network/cli/cli_module.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "71adedc22e395cc0d3028043739c5a5b55cf9116e5b9661a14b6011849492921", + "format": 1 + }, + { + "name": "tests/unit/modules/network/cli/test_cli_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e1fe40d6fb4ecef4a8b1af542c0254c5f37c80cfce65ea9f8404c72fd229c7de", + "format": 1 + }, + { + "name": "tests/unit/modules/network/cli/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/modules/network/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/modules/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/.gitignore", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b5726d3ec9335a09c124469eca039523847a6b0f08a083efaefd002b83326600", + "format": 1 + }, + { + "name": "tests/sanity", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.15.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.19.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.14.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.21.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.20.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.22.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.16.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.17.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.18.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7a76e6b5a6a1b75c786e571fe854f37b69f1bf44883efae7b9efa0578ca98183", + "format": 1 + }, + { + "name": "requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0ccccf396f00eee9e232dd5f96a4287d1267614e4568099173259075e7588056", + "format": 1 + }, + { + "name": "LICENSES", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "LICENSES/PSF-2.0.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bb27f2829b315447199ab930b6d0e0ae6716754ebfa4ccf81f6eb7738b0f963a", + "format": 1 + }, + { + "name": "LICENSES/BSD-2-Clause.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f6036f79d054f42e11f2dd52458b4d2282e901d197955e598bf1a23600280cf0", + "format": 1 + }, + { + "name": "LICENSES/GPL-3.0-or-later.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986", + "format": 1 + }, + { + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/runtime.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9be8419093ffb6686b9f010a57b5362d68736f69bb1fb8e604036117a734b644", + "format": 1 + }, + { + "name": "tox-ansible.ini", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7b76fb1ae48b358f6d8b8d145474a2bb70f1915b9340af8ec1e1313a304bc633", + "format": 1 + }, + { + "name": "LICENSE", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986", + "format": 1 + }, + { + "name": "docs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/ansible.netcommon.telnet_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e75f5a0914a102ce7fdc0ffb83933a5afad1228f04c72b4855da9154f5403cf7", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.network_resource_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5a2be201e2815cdbb047d05e98d735e8130fe47f99b9ce9ea3a3c971f78f2824", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.parse_cli_textfsm_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "87aaac7fe0ac137bd36d23510de91217babfaa160bffa8dafa55b8024a096173", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.grpc_connection.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "214da8572bccb96126741d4545eec467cf502d75810f6b0d957814c8b4cebb60", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.net_get_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "53a379e03e2889b6cfa119cf9f9d8c8d9dfacad3fad45596c5f90ab6ce2a34af", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.cli_backup_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5b222764f996ccac0e510c3c2d77490fc519935aadbe8edc91b88f0faae0e5ba", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.cli_command_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e22b622eab92c49e6762133d7de8498e751809407833e062034663c2006be16a", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.net_put_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "51cb81fc87bc4e92c6442a15d218fa9425bf8b19cb47f33e3e7a7c9b6c1cc560", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.type5_pw_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "97d834f806c103f4efd6a51e6f38150f23d0dc8a03b2b33251c083f7ae160758", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.vlan_expander_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "11c2031e24c740975c682ba42257ee4f732b0d6931e31ed872021a1ae87e5ee9", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.hash_salt_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "078b178b001e71ed8a6c01a24a88d2ccbdfcb043e5baf31def2c917b3361b571", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.netconf_config_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "988384818ac3f45f51ec7e97ee6fb19614665b293c90c45426286d250badebac", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.net_ping_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ac33d21c4f133625feb63e1edc99621fa9a37db0b5dae6728dbc54911fee12cf", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.pop_ace_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a21318e04f271de018ab19b084078ef0ce9292396f839f4d1f10d9d96d245aad", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.grpc_config_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cdfd7cd10a332cd9477427ecb8eb1123671455404cbbe1ea62d6da0ee88ccb84", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.vlan_parser_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "227c999acc24c259a7eda976e8fc100a8655a7bfacb92a419324c2d843791bcf", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.restconf_get_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ec64e2c0ec91b10bc1df3cdb3ce58076615f7f258b3ffc1032e8efe9237fd7a", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.netconf_rpc_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b8ed5ab30aa2c64bd979e916534335039b3ecacdea98cf0ad031ca63e7d51148", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.default_netconf.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "60610f9b4e18905f854afbdd8a6cd0549acf58c99fb212b46d8de1d9e46e6ff6", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.enable_become.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b56103797876c3da488319d7ce0f9e9bc0386c05989f3e7b0af949692a6054bc", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.httpapi_connection.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "11062a9bf605eb1581dd095eb0d06cc5eb9c769ab2ba2d91d9a66c013a350832", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.libssh_connection.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "32e6d06b0ea62dd6f0560b00a0bd45ad4da306b5180cf1f8431820e961dc8a5d", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.comp_type5_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ce772bc89d40db72dfb2a875e2403133a45465852852a8341c99acd8392123a", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.persistent_connection.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8cc16cbadad74f2ff57c9da4af2326fcbc313ebb5e999850179e7096c999a38e", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.netconf_connection.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "22450121a47b65e34bf523dc49d4425466eb6e1032e94b4d012cdcf7f6ed0854", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.network_cli_connection.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "20a1ed26ebd4feb94afb7f3549953e5c1ab4723a1d536be56127eb044c64dead", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.restconf_config_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8202fa525ea6fe95d25f578c612b7aab8cc75d97c0f59b233420cfb342de807c", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.cli_config_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cf906e2b742a1f225cb48569dcb77e8c95d564c034bc2f5775c32bb7fc02d5b4", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.parse_cli_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bec45126945301441346e3e41e3e70422d62b0fae51cc7051733457b1b60fbe0", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.restconf_httpapi.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9dfac86340b98f8cefbb7046415e72d1a6dd3b33ec40b3111888a01e6650ec43", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.default_cliconf.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b5602111676d608c1af8e72bd589572838fba44ec2cf3104ef19f9546dc57993", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.grpc_get_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "03f346c20e371825edbc33ea56d62b792f7b5ae62f1c901487e779f8256616f9", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.netconf_get_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3eb34b2cbf47fa8a03e76979673865d1b351604394a03e2232a234d0961405db", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.parse_xml_filter.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4d6188fb0dcfccecbede41280fc83a9cd203afe4d7407bd3b184cd9531b65c4d", + "format": 1 + }, + { + "name": "docs/ansible.netcommon.cli_restore_module.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ce4f913d4d9d928cdeb39067a3804a2a88b940022f7f856b779b885868bf0390", + "format": 1 + }, + { + "name": "test-requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a2dbb658a407ce4e4e6fa2ceca99235093bdb1e205fc9f98b46992270a2003e8", + "format": 1 + }, + { + "name": "bindep.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "760817337c6730981eb9fde5d2f43c903f5a2522f0876b9d4b1f5c09d298a56f", + "format": 1 + }, + { + "name": "plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/sub_plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/sub_plugins/grpc", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/sub_plugins/grpc/base.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "574c09c370cecbb87a829e3f2fa1c8aa4afc287ad2d06654e06aa220d9e3fa5f", + "format": 1 + }, + { + "name": "plugins/sub_plugins/cli_parser", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/sub_plugins/cli_parser/ntc_templates_parser.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0dfc695535ec20952d0cc2a3c3c3198407cfe858a6a3f6a41eba92afd8a4fb6c", + "format": 1 + }, + { + "name": "plugins/sub_plugins/cli_parser/content_templates_parser.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "110cea21ab2f350549f20d459e5d6f8c3dc58043e284f639e88d8aab59158fb1", + "format": 1 + }, + { + "name": "plugins/sub_plugins/cli_parser/pyats_parser.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "407cf47657aa33b212bda14d15465c0fd51cac46a1aeecfef229ad90b0b2510e", + "format": 1 + }, + { + "name": "plugins/sub_plugins/cli_parser/native_parser.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8e8a9dab68007fab447d14ac882a628e8e829f6e8731ce09a758840a58472a89", + "format": 1 + }, + { + "name": "plugins/become", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/become/enable.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0bbcfd4eac913f404d25ae35209d7f3ba62989009f6fa73be8a4d39de3b48ef4", + "format": 1 + }, + { + "name": "plugins/doc_fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/doc_fragments/connection_persistent.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5282e3baa0e06d00f59879b69ec959864189a27e6cb1f1020e13d4759f76d81c", + "format": 1 + }, + { + "name": "plugins/doc_fragments/network_agnostic.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7c296eab68967ae69f6ca50923781f1971e8e3ec1db26d51d5c9c84916c66dcb", + "format": 1 + }, + { + "name": "plugins/cliconf", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/cliconf/default.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "05007d881eebc12579435d8ce26e5c981786e6d1966958d061eaeef3b29b5742", + "format": 1 + }, + { + "name": "plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/utils/data.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ead5b9015092218549edfd2ddb5c3b2612c939e498fca9ed8f3f685ea8bcd3ec", + "format": 1 + }, + { + "name": "plugins/module_utils/network", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/network/restconf", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/network/restconf/restconf.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "befb0fefc07679bcddf18d67e21d608e3ed9fe94cfa8a137ffe6e91a5d23c92e", + "format": 1 + }, + { + "name": "plugins/module_utils/network/grpc", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/network/grpc/grpc.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "768188794f32a537629776eff1a4b342aab54ef1e036a969285b14d8507584d6", + "format": 1 + }, + { + "name": "plugins/module_utils/network/netconf", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/network/netconf/netconf.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "db3d9b893b24b86b6f1a980d66bb5ad9662addb1198a37518584f4fd48b938c9", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "01a322706a80dc6f9727c62263fd3c89bde1db27b68f2ddd43960065bc9efde0", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e8cdad29715e7eebef59cb9cc72345f92add24885b88920345f4edd53ff8354e", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/network.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c7dbed9ffff9835ed6e1c433dcfe9fdd053ec420e7be57a6b1439c94aa8700c5", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/facts", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/facts/facts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0ce9d3cb612543f57dddbf9c3180638442bf9665e0586613a84e3d19e2a414ff", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/cfg", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/cfg/base.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1bcd73c6b06714fd0d42365bc4b9613e9353519747bf73628f4392b60f697a35", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/netconf.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1b4e01d1eab7242b214d67d8fba53cc5fa4a914af625d90f8faf388ad06afd46", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/rm_base", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/rm_base/network_template.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d9233525265c54193dd1bf061d4f09527c2bf891fabda993b84cff94f57c1869", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/rm_base/resource_module.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5cb6abe87e749395e561358871af00218dde17b81aaeee506cc468b36288229c", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/rm_base/resource_module_base.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1f4a1af713cb0702623a786196097b8d7371d81ecc2a9d8b9b2a07590954e29f", + "format": 1 + }, + { + "name": "plugins/module_utils/network/common/parsing.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7981b86b8c605d2b379af818d6ae064122ade36065ac04e84f9da5f55257e7ed", + "format": 1 + }, + { + "name": "plugins/module_utils/cli_parser", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/cli_parser/cli_parsertemplate.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8958d37ffa710f6ebe83e83330b1be5a1ff4b302bb3db7e7fc775b9e27224d21", + "format": 1 + }, + { + "name": "plugins/module_utils/cli_parser/cli_parserbase.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7744c2e8dc35994dc90085c20ecc3725c331502335426febbed4d740e6cbe741", + "format": 1 + }, + { + "name": "plugins/connection", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/connection/persistent.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4b52303f927d856e491b8b8add51e2c66dee3c4c90afeb7f83c9614c7f735de8", + "format": 1 + }, + { + "name": "plugins/connection/grpc.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9e91e5595a2d4537340de6d14fd60c4a465948d533ad94d136359854c683d998", + "format": 1 + }, + { + "name": "plugins/connection/netconf.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d2deed4efdce02c5e0d69dda8d59fcf299131bf4769d118e713af5693fac4ade", + "format": 1 + }, + { + "name": "plugins/connection/libssh.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ab35945300ddf9cb0b22a19e3847abd9a24bfcdcd03dfd08436c390d1b6081a3", + "format": 1 + }, + { + "name": "plugins/connection/network_cli.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "491607f7a3c72e67c86be543972a3a172eef97701440a5ec174d9aa10cdc61ea", + "format": 1 + }, + { + "name": "plugins/connection/httpapi.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "694f5287cb4dc3b4e16891273b83c68f59aaaf0e54803cc71b64e7e5c9884f82", + "format": 1 + }, + { + "name": "plugins/action", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/action/cli_command.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6a944d8f77789b2d0dad27adbb438be747141fa4e415ee32784b934b3bf35eec", + "format": 1 + }, + { + "name": "plugins/action/net_ping.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "272e933618a9020df35d879662acf8c2c9f771496e64b2ce2a27a0b114092aa2", + "format": 1 + }, + { + "name": "plugins/action/network_resource.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "30cafd80141d6940160bd87377418c1c9e46cb741e5642ac20eae359d3c4a627", + "format": 1 + }, + { + "name": "plugins/action/network.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "380dab9293d574dddbd451cb13d64982aef30f627d86e941bedba149e8f221a0", + "format": 1 + }, + { + "name": "plugins/action/grpc.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2722eb0c61acd2fa06155215e99cb0f547690fb3fd5a5b4e656fad90ad091dea", + "format": 1 + }, + { + "name": "plugins/action/netconf.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "93fbb55498f154df133f290d962100b0ee49e6a4108ad586e23fc27f90bbb4a8", + "format": 1 + }, + { + "name": "plugins/action/cli_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fb4dc58e8b24d2167d5b2e439c9e685160ca577d43589c0aa211b9b61202c5a4", + "format": 1 + }, + { + "name": "plugins/action/net_get.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f68e8d5d655cccf799cbfc9287f6d9ae4576254520971f403954c6c1ee0f8d40", + "format": 1 + }, + { + "name": "plugins/action/net_put.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5321294964498656dcc97ce08247d19978e22119e03b8218d4dbc09bff218d7a", + "format": 1 + }, + { + "name": "plugins/action/cli_backup.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "50af044056f3bd7e53e235da97d782556b65322c32dc38354212b8f83d08774b", + "format": 1 + }, + { + "name": "plugins/action/telnet.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "49d0d8305f958c29efa7a61d7535277a41cfed6b2eec0bb2bd50445c0cf0c2ee", + "format": 1 + }, + { + "name": "plugins/netconf", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/netconf/default.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8402cab7a6997a1a2e82ab401b34ecb397de151ace67e4d17988409192eb975f", + "format": 1 + }, + { + "name": "plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/modules/cli_restore.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1cf484b8565709886dd769c278a7566c83c91b08045d453cf4946d9ff566c5c0", + "format": 1 + }, + { + "name": "plugins/modules/cli_command.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0745ddfc6f47efc1c7c9474e72b9a4f5d6f76a8c9eb90c045cc3e3fb73a9cad6", + "format": 1 + }, + { + "name": "plugins/modules/restconf_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4783f894c1b8654827e38cf2e36186352d7d23eee6ce41c5e79ec0c743a2d9b0", + "format": 1 + }, + { + "name": "plugins/modules/net_ping.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "22089b045eb7c35003893cbffac47304e16e9bc4f3b4fc1ea17a4d8d34a41524", + "format": 1 + }, + { + "name": "plugins/modules/netconf_rpc.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1ac88505fa9ba51da7cd97381cba46448c615be2025031ea6cba45b05b440211", + "format": 1 + }, + { + "name": "plugins/modules/grpc_get.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1c9b2f7309b07d9c59ad69bcc0a7d9be45cf4550423d29d7da9121e750b5c254", + "format": 1 + }, + { + "name": "plugins/modules/restconf_get.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e15be308d814fcdc2048bdac35ac68cdfc1311f6a6cb8b1f0497b3704fca4233", + "format": 1 + }, + { + "name": "plugins/modules/network_resource.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cf30880d4a98ffffd07ad6dd6e5d129bc4d201655267e2da33a03212d0ba53e5", + "format": 1 + }, + { + "name": "plugins/modules/cli_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b70ac602d62edca23da00e131a568e6ef222943cedfd9c5c0f1d0e8dbc767bdc", + "format": 1 + }, + { + "name": "plugins/modules/netconf_get.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3832a4b94088bbd44eab178a60fd9fb51f1ff775859d4481de95a332747dfef4", + "format": 1 + }, + { + "name": "plugins/modules/net_get.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2648915107fab66fde9e6d90d698445ba675974b702a1847354eb7ad8bd3600e", + "format": 1 + }, + { + "name": "plugins/modules/net_put.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cb0fac80bee26d7417df12681dc714dc3cfba1862ce8468ec4d32c4ab2a81746", + "format": 1 + }, + { + "name": "plugins/modules/grpc_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "28b286723b9b1a74883e1bcc6a10996c92eca9275d527b8396882291d04256df", + "format": 1 + }, + { + "name": "plugins/modules/cli_backup.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cda0d891761650ceb6da32bf20eb63c74d611b8bf51a595df47e66e2a7da2da6", + "format": 1 + }, + { + "name": "plugins/modules/telnet.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4fd508b2faf1c6ab7042887e03197d90fc19a3832894db88462529ac4a729bf4", + "format": 1 + }, + { + "name": "plugins/modules/netconf_config.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f4cae05817f3ed7d4f8884a4a5ad1af09bcbb0b4b12dadf280ecb2f9b6810729", + "format": 1 + }, + { + "name": "plugins/plugin_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/plugin_utils/hash_salt.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0696153374e089369f61a6625ce81988e8e9d9cd09b5e90f1d65dba5cc651bf6", + "format": 1 + }, + { + "name": "plugins/plugin_utils/cliconf_base.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0b6618ebc3f8a39f35e5b77f6987b8b391f31f31dc77970f5b85ed5565c60b11", + "format": 1 + }, + { + "name": "plugins/plugin_utils/parse_cli_textfsm.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a2c27172d45011e58e09f7eacfee8115c6d604e362f66192046c0260c2e9236f", + "format": 1 + }, + { + "name": "plugins/plugin_utils/terminal_base.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7371a16a1f015e67bbbe2fe55fc1e6ac3c93ddf92cc2f319e5931bdb3f202591", + "format": 1 + }, + { + "name": "plugins/plugin_utils/connection_base.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8d332bd4dfd00daf050050d2d91b1d07e5f93b38166d862f752a4145569f9397", + "format": 1 + }, + { + "name": "plugins/plugin_utils/parse_xml.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0da43dd232bb8ca4cab38257db56b72262f2a82ed69eb691e235374f313b0d31", + "format": 1 + }, + { + "name": "plugins/plugin_utils/comp_type5.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a181233e662f2bd8885e774e6231a9dff3ff05126a40cb6e6b9d79051ec6e401", + "format": 1 + }, + { + "name": "plugins/plugin_utils/httpapi_base.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "476a70a781bfac0e430733345bab55715ba638b1a6956c38add91745281f2d4f", + "format": 1 + }, + { + "name": "plugins/plugin_utils/netconf_base.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "15fb5e15c5637ed1208a3c38b536a97a40d6aaaba90415e4ad6ef5e75561cf55", + "format": 1 + }, + { + "name": "plugins/plugin_utils/vlan_parser.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "32e8e991937ab6bc643a307ba676ac997a975fb8a78ffccda88d1d751567b3bf", + "format": 1 + }, + { + "name": "plugins/plugin_utils/version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3f562bd90163cff9a7b0af4ac2fffa2cd8ce61b6e85d65ffe25b0562f0d8aa8e", + "format": 1 + }, + { + "name": "plugins/plugin_utils/pop_ace.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3fe9aaa356aadf16d948fa95e158f8c871f52f0a185a3d5f1efc73865f0f327b", + "format": 1 + }, + { + "name": "plugins/plugin_utils/parse_cli.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f89bba5b197c5139de10328260bb7e03ac537a0f56c52cd04f6073fe97d99f63", + "format": 1 + }, + { + "name": "plugins/plugin_utils/vlan_expander.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cd627a4cdb9367c8baa9a55c0ad44fd6a77d863f0e2c11e737da74612ea8bdc2", + "format": 1 + }, + { + "name": "plugins/plugin_utils/type5_pw.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d5d74f4c2869df8ca8f8887bab28952d8fe2af87664cf9e976ea37a0a3841157", + "format": 1 + }, + { + "name": "plugins/plugin_utils/compat", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/plugin_utils/compat/telnetlib.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4378620e3a3db29cd79d86ba04dd3b15c06b0bf00373f42c9990c7db28815ba9", + "format": 1 + }, + { + "name": "plugins/plugin_utils/argspec_filter_utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "861bdaf23d59d41dd7aa791aeef389dafa7092dc566338cd74c17488e926fe75", + "format": 1 + }, + { + "name": "plugins/httpapi", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/httpapi/restconf.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ed2ec4e2e12e0ae05ec41bfa8efbeecec2655fd4ccfe4ee1c079f426aa5b1606", + "format": 1 + }, + { + "name": "plugins/cache", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/cache/memory.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "741033bfd18a35bc814b44e46fd213f5bc4b078b62dfa556baac1079211e3ea5", + "format": 1 + }, + { + "name": "plugins/filter", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/filter/hash_salt.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2a5b837b328df319308dc3512173df28d15e0583dd89dbda1abb4bff1a5d65e8", + "format": 1 + }, + { + "name": "plugins/filter/parse_cli_textfsm.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eef103778682955aeb531018b2b3fea489b2682026dfe1276099ebaa9be2af40", + "format": 1 + }, + { + "name": "plugins/filter/parse_xml.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4c36c663b04d6b68366fc5c1630b48a952c1af8d5b2a9d7cb88d575073979035", + "format": 1 + }, + { + "name": "plugins/filter/comp_type5.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "837722a51189ff30517488f342147e747da3b3dbc799325d22e3077924ade684", + "format": 1 + }, + { + "name": "plugins/filter/vlan_parser.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4e9665cbbd4c8250d1da5afb92a7ebf3cd8a2397560e7a26aacc2961f5f4e85e", + "format": 1 + }, + { + "name": "plugins/filter/pop_ace.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "048627c1afde305d581bb9b5abf8cf913f24178e56a4b966569073b3d450074b", + "format": 1 + }, + { + "name": "plugins/filter/parse_cli.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "43cb3bf0dcc79461fae3cdfb5aa29d83beb0e995ad413ec0e77be9395df8e9b7", + "format": 1 + }, + { + "name": "plugins/filter/vlan_expander.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a68fa3259086531f0961950f3664b6bd074028f94b2b580eba8a947c0a96e6bb", + "format": 1 + }, + { + "name": "plugins/filter/type5_pw.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0db0c06d515023fec5cc0e785efd53a4e49ed34fbeb6795415aad71cba9b2cfc", + "format": 1 + }, + { + "name": "CHANGELOG.rst", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "150b011cfc7c59355099550dbf60e27b88da1e002cbf3ffe161d971ad2279bb3", + "format": 1 + }, + { + "name": "pyproject.toml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3ff9699724c6eb8760860135965bbc4284f1ae2d13bc9649a94f0713dee0b4d5", + "format": 1 + } + ], + "format": 1 +} \ No newline at end of file diff --git a/collections/ansible_collections/ansible/netcommon/LICENSE b/collections/ansible_collections/ansible/netcommon/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/collections/ansible_collections/ansible/netcommon/LICENSES/BSD-2-Clause.txt b/collections/ansible_collections/ansible/netcommon/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000..e347639 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,7 @@ +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/collections/ansible_collections/ansible/netcommon/LICENSES/GPL-3.0-or-later.txt b/collections/ansible_collections/ansible/netcommon/LICENSES/GPL-3.0-or-later.txt new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/collections/ansible_collections/ansible/netcommon/LICENSES/PSF-2.0.txt b/collections/ansible_collections/ansible/netcommon/LICENSES/PSF-2.0.txt new file mode 100644 index 0000000..5fe8a31 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/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, 2022, 2023 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/collections/ansible_collections/ansible/netcommon/MANIFEST.json b/collections/ansible_collections/ansible/netcommon/MANIFEST.json new file mode 100644 index 0000000..1f5e1f0 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/MANIFEST.json @@ -0,0 +1,38 @@ +{ + "collection_info": { + "namespace": "ansible", + "name": "netcommon", + "version": "8.5.0", + "authors": [ + "Ansible Network Community (ansible-network)" + ], + "readme": "README.md", + "tags": [ + "networking", + "security", + "cloud", + "network_cli", + "netconf", + "httpapi", + "grpc" + ], + "description": "Ansible Collection with common content to help automate the management of network, security, and cloud devices.", + "license": [], + "license_file": "LICENSE", + "dependencies": { + "ansible.utils": ">=3.0.0" + }, + "repository": "https://github.com/ansible-collections/ansible.netcommon", + "documentation": null, + "homepage": null, + "issues": "https://github.com/ansible-collections/ansible.netcommon/issues" + }, + "file_manifest_file": { + "name": "FILES.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "66c2b1ae59994d46657d9ba6343ee5e0497e2bd4313805bb506db7b86d41ffdc", + "format": 1 + }, + "format": 1 +} \ No newline at end of file diff --git a/collections/ansible_collections/ansible/netcommon/README.md b/collections/ansible_collections/ansible/netcommon/README.md new file mode 100644 index 0000000..8e9d811 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/README.md @@ -0,0 +1,172 @@ + + +# Ansible Network Collection for Common Code (netcommon) + +[![Integration tests 💻](https://github.com/ansible-collections/ansible.netcommon/actions/workflows/integration-cml.yml/badge.svg?branch=main)](https://github.com/ansible-collections/ansible.netcommon/actions/workflows/integration-cml.yml) +[![CI](https://github.com/ansible-collections/ansible.netcommon/actions/workflows/tests.yml/badge.svg?branch=main&event=schedule)](https://github.com/ansible-collections/ansible.netcommon/actions/workflows/tests.yml) + +The Ansible ``ansible.netcommon`` collection includes common content to help automate the management of network, security, and cloud devices. +This includes connection plugins, such as ``network_cli``, ``httpapi``, and ``netconf``. + +## Support + +As Red Hat Ansible Certified Content, this collection is entitled to support through the Ansible Automation Platform (AAP) using the **Create issue** button on the top right corner. If a support case cannot be opened with Red Hat and the collection has been obtained either from Galaxy or GitHub, there may community help available on the [Ansible Forum](https://forum.ansible.com/). + +Additionally, you can join us on [#network:ansible.com](https://matrix.to/#/#network:ansible.com) room or the [Ansible Forum Network Working Group](https://forum.ansible.com/g/network-wg). + +For more information you can check the communication section below. + +## Communication + +* Join the Ansible forum: + * [Get Help](https://forum.ansible.com/c/help/6): get help or help others. + * [Posts tagged with 'network'](https://forum.ansible.com/tag/network): subscribe to participate in collection-related conversations. + * [Ansible Network Automation Working Group](https://forum.ansible.com/g/network-wg/): by joining the team you will automatically get subscribed to the posts tagged with [network](https://forum.ansible.com/tags/network). + * [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts. + * [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events. + +* The Ansible [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn): used to announce releases and important changes. + +For more information about communication, see the [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html). + + +## Ansible version compatibility + +This collection has been tested against the following Ansible versions: **>=2.16.0**. + +Plugins and modules within a collection may be tested with only specific Ansible versions. +A collection may contain metadata that identifies these versions. +PEP440 is the schema used to describe the versions of Ansible. + + +## Included content + + +### Become plugins +Name | Description +--- | --- +[ansible.netcommon.enable](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.enable_become.rst)|Switch to elevated permissions on a network device + +### Cliconf plugins +Name | Description +--- | --- +[ansible.netcommon.default](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.default_cliconf.rst)|General purpose cliconf plugin for new platforms + +### Connection plugins +Name | Description +--- | --- +[ansible.netcommon.grpc](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.grpc_connection.rst)|Provides a persistent connection using the gRPC protocol +[ansible.netcommon.httpapi](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.httpapi_connection.rst)|Use httpapi to run command on network appliances +[ansible.netcommon.libssh](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.libssh_connection.rst)|Run tasks using libssh for ssh connection +[ansible.netcommon.netconf](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.netconf_connection.rst)|Provides a persistent connection using the netconf protocol +[ansible.netcommon.network_cli](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.network_cli_connection.rst)|Use network_cli to run command on network appliances +[ansible.netcommon.persistent](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.persistent_connection.rst)|Use a persistent unix socket for connection + +### Filter plugins +Name | Description +--- | --- +[ansible.netcommon.comp_type5](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.comp_type5_filter.rst)|The comp_type5 filter plugin. +[ansible.netcommon.hash_salt](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.hash_salt_filter.rst)|The hash_salt filter plugin. +[ansible.netcommon.parse_cli](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.parse_cli_filter.rst)|parse_cli filter plugin. +[ansible.netcommon.parse_cli_textfsm](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.parse_cli_textfsm_filter.rst)|parse_cli_textfsm filter plugin. +[ansible.netcommon.parse_xml](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.parse_xml_filter.rst)|The parse_xml filter plugin. +[ansible.netcommon.pop_ace](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.pop_ace_filter.rst)|Remove ace entries from a acl source of truth. +[ansible.netcommon.type5_pw](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.type5_pw_filter.rst)|The type5_pw filter plugin. +[ansible.netcommon.vlan_expander](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.vlan_expander_filter.rst)|The vlan_expander filter plugin. +[ansible.netcommon.vlan_parser](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.vlan_parser_filter.rst)|The vlan_parser filter plugin. + +### Httpapi plugins +Name | Description +--- | --- +[ansible.netcommon.restconf](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.restconf_httpapi.rst)|HttpApi Plugin for devices supporting Restconf API + +### Netconf plugins +Name | Description +--- | --- +[ansible.netcommon.default](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.default_netconf.rst)|Use default netconf plugin to run standard netconf commands as per RFC + +### Modules +Name | Description +--- | --- +[ansible.netcommon.cli_backup](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.cli_backup_module.rst)|Back up device configuration from network devices over network_cli +[ansible.netcommon.cli_command](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.cli_command_module.rst)|Run a cli command on cli-based network devices +[ansible.netcommon.cli_config](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.cli_config_module.rst)|Push text based configuration to network devices over network_cli +[ansible.netcommon.cli_restore](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.cli_restore_module.rst)|Restore device configuration to network devices over network_cli +[ansible.netcommon.grpc_config](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.grpc_config_module.rst)|Fetch configuration/state data from gRPC enabled target hosts. +[ansible.netcommon.grpc_get](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.grpc_get_module.rst)|Fetch configuration/state data from gRPC enabled target hosts. +[ansible.netcommon.net_get](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.net_get_module.rst)|Copy a file from a network device to Ansible Controller +[ansible.netcommon.net_ping](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.net_ping_module.rst)|Tests reachability using ping from a network device +[ansible.netcommon.net_put](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.net_put_module.rst)|Copy a file from Ansible Controller to a network device +[ansible.netcommon.netconf_config](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.netconf_config_module.rst)|netconf device configuration +[ansible.netcommon.netconf_get](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.netconf_get_module.rst)|Fetch configuration/state data from NETCONF enabled network devices. +[ansible.netcommon.netconf_rpc](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.netconf_rpc_module.rst)|Execute operations on NETCONF enabled network devices. +[ansible.netcommon.network_resource](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.network_resource_module.rst)|Manage resource modules +[ansible.netcommon.restconf_config](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.restconf_config_module.rst)|Handles create, update, read and delete of configuration data on RESTCONF enabled devices. +[ansible.netcommon.restconf_get](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.restconf_get_module.rst)|Fetch configuration/state data from RESTCONF enabled devices. +[ansible.netcommon.telnet](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.telnet_module.rst)|Executes a low-down and dirty telnet command + + + + +## Installing this collection + +You can install the ``ansible.netcommon`` collection with the Ansible Galaxy CLI: + + ansible-galaxy collection install ansible.netcommon + +You can also include it in a `requirements.yml` file and install it with `ansible-galaxy collection install -r requirements.yml`, using the format: + +```yaml +--- +collections: + - name: ansible.netcommon +``` +## Using this collection + +The most common use case for this collection is to include it as a dependency in a network device-specific collection. Use the Fully Qualified Collection Name (FQCN) when referring to content in this collection (for example, `ansible.netcommon.network_cli`). + +See the [Vyos collection](https://github.com/ansible-collections/vyos.vyos) for an example of this. + +### See Also: + +* [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for more details. + +## Contributing to this collection + +We welcome community contributions to this collection. If you find problems, please open an issue or create a PR against the [ansible.netcommon collection repository](https://github.com/ansible-collections/ansible.netcommon). See [Contributing to Ansible-maintained collections](https://docs.ansible.com/ansible/devel/community/contributing_maintained_collections.html#contributing-maintained-collections) for complete details. + +You can also join us on: + +- IRC - ``#ansible-network`` [irc.libera.chat](https://libera.chat/) channel +- Slack - https://ansiblenetwork.slack.com + +See the [Ansible Community Guide](https://docs.ansible.com/ansible/latest/community/index.html) for details on contributing to Ansible. + +### Code of Conduct +This collection follows the Ansible project's +[Code of Conduct](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html). +Please read and familiarize yourself with this document. + + +## Release notes + +Release notes are available [here](https://github.com/ansible-collections/ansible.netcommon/blob/main/CHANGELOG.rst) + +## Roadmap + + + +## More information + +- [Developing network resource modules](https://docs.ansible.com/ansible/latest/network/dev_guide/developing_resource_modules_network.html#developing-resource-modules) +- [Ansible network resources](https://docs.ansible.com/ansible/latest/network/getting_started/network_resources.html) +- [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 Community code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) + +## Licensing + +GNU General Public License v3.0 or later. + +See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text. diff --git a/collections/ansible_collections/ansible/netcommon/bindep.txt b/collections/ansible_collections/ansible/netcommon/bindep.txt new file mode 100644 index 0000000..7efab69 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/bindep.txt @@ -0,0 +1,28 @@ +# This is a cross-platform list tracking distribution packages needed by tests; +# see https://docs.openstack.org/infra/bindep/ for additional information. + +gcc-c++ [doc test platform:rpm] +libyaml-devel [test platform:rpm] +libyaml-dev [test platform:dpkg] + +# ansible-pylibssh +gcc [compile platform:rpm] +libssh-dev [compile platform:dpkg] +libssh-devel [compile platform:rpm] +python3-Cython [compile platform:fedora-35 platform:rhel-9] + +# ansible-pylibssh (runtime) +libssh [platform:rpm] +libssh-4 [platform:dpkg] + +# ncclient +python3-six [platform:centos-9 platform:rhel-9] +python3-lxml [platform:centos-9 platform:rhel-9] + +# paramiko +findutils [compile platform:centos-8 platform:rhel-8] +gcc [compile platform:centos-8 platform:rhel-8] +make [compile platform:centos-8 platform:rhel-8] +python3-cffi [platform:centos-9 platform:rhel-9] +python3-cryptography [platform:centos-9 platform:rhel-9] +python3-pycparser [platform:centos-9 platform:rhel-9] diff --git a/collections/ansible_collections/ansible/netcommon/changelogs/CHANGELOG.rst b/collections/ansible_collections/ansible/netcommon/changelogs/CHANGELOG.rst new file mode 100644 index 0000000..cd3e8e4 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/changelogs/CHANGELOG.rst @@ -0,0 +1 @@ +The changelog has moved `here `_ diff --git a/collections/ansible_collections/ansible/netcommon/changelogs/changelog.yaml b/collections/ansible_collections/ansible/netcommon/changelogs/changelog.yaml new file mode 100644 index 0000000..ec17dfc --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/changelogs/changelog.yaml @@ -0,0 +1,994 @@ +ancestor: null +releases: + 1.0.0: + modules: + - description: Run a cli command on cli-based network devices + name: cli_command + namespace: "" + - description: Push text based configuration to network devices over network_cli + name: cli_config + namespace: "" + - description: Copy a file from a network device to Ansible Controller + name: net_get + namespace: "" + - description: Tests reachability using ping from a network device + name: net_ping + namespace: "" + - description: Copy a file from Ansible Controller to a network device + name: net_put + namespace: "" + - description: netconf device configuration + name: netconf_config + namespace: "" + - description: Fetch configuration/state data from NETCONF enabled network devices. + name: netconf_get + namespace: "" + - description: Execute operations on NETCONF enabled network devices. + name: netconf_rpc + namespace: "" + - description: + Handles create, update, read and delete of configuration data on + RESTCONF enabled devices. + name: restconf_config + namespace: "" + - description: Fetch configuration/state data from RESTCONF enabled devices. + name: restconf_get + namespace: "" + - description: Executes a low-down and dirty telnet command + name: telnet + namespace: "" + plugins: + become: + - description: Switch to elevated permissions on a network device + name: enable + namespace: null + connection: + - description: Use httpapi to run command on network appliances + name: httpapi + namespace: null + - description: Provides a persistent connection using the netconf protocol + name: netconf + namespace: null + - description: Use network_cli to run command on network appliances + name: network_cli + namespace: null + - description: Use a persistent unix socket for connection + name: persistent + namespace: null + httpapi: + - description: HttpApi Plugin for devices supporting Restconf API + name: restconf + namespace: null + netconf: + - description: + Use default netconf plugin to run standard netconf commands as + per RFC + name: default + namespace: null + release_date: "2020-06-23" + 1.1.0: + changes: + bugfixes: + - Replace deprecated `getiterator` call with `iter` + - ipaddr - "host" query supports /31 subnets properly + - ipaddr filter - Fixed issue where the first IPv6 address in a subnet was not + being considered a valid address. + - ipaddr filter now returns empty list instead of False on empty list input + - net_put - Restore missing function removed when action plugin stopped inheriting + NetworkActionBase + - nthhost filter now returns str instead of IPAddress object + - slaac filter now returns str instead of IPAddress object + major_changes: + - Add libssh connection plugin and refactor network_cli (https://github.com/ansible-collections/ansible.netcommon/pull/30) + minor_changes: + - Add content option validation for netconf_config module (https://github.com/ansible-collections/ansible.netcommon/pull/66) + - Documentation of module arguments updated to match expected types where missing. + - "Resource Modules: changed flag is set to true in check_mode for all ACTION_STATES + (https://github.com/ansible-collections/ansible.netcommon/pull/82)" + removed_features: + - module_utils.network.common.utils.ComplexDict has been removed + fragments: + - 103-net-put-handle-src.yaml + - 30-add-libssh-connection-support.yaml + - 34-ipaddr-empty-list.yaml + - 66-netconf-config-vaildation.yml + - 72-ipv6-first-address-fix.yaml + - 74-remove-getiterator.yaml + - 75-unit-tests.yaml + - 78-sanity-cleanup.yaml + - 82-changed_true_action_states_check_mode_yes.yml + - 95-ipaddr.yaml + release_date: "2020-07-30" + 1.1.1: + changes: + release_summary: Rereleased 1.1.0 with regenerated documentation. + fragments: + - 1.1.1.yaml + release_date: "2020-07-31" + 1.1.2: + changes: + release_summary: Rereleased 1.1.1 with updated changelog. + fragments: + - 1.1.2.yaml + release_date: "2020-08-06" + 1.2.0: + changes: + bugfixes: + - cli_config fixes issue when rollback_id = 0 evalutes to False + - sort_list will sort a list of dicts using the sorted method with key as an + argument. + minor_changes: + - Added description to collection galaxy.yml file. + - NetworkConfig objects now have an optional `comment_tokens` parameter which + takes a list of strings which will override the DEFAULT_COMMENT_TOKENS list. + - New cli_parse module for parsing structured text using a variety of parsers. + The initial implemetation of cli_parse can be used with json, native, ntc_templates, + pyats, textfsm, ttp, and xml. + - The httpapi connection plugin now works with `wait_for_connection`. This will + periodically request the root page of the server described by the plugin's + options until the request succeeds. This can only test that the server is + reachable, the correctness or usability of the API is not guaranteed. + fragments: + - 105-wait_for_conn-httpapi.yaml + - 109-cli_parse_module_addition.yaml + - 110-NetworkConfig-comments.yaml + - 114-sort_list_listofdicts.yaml + - 118-cli_config.yaml + - 127-galaxy-fragment.yaml + release_date: "2020-08-25" + 1.2.1: + changes: + bugfixes: + - Fixed "Object of type Capabilities is not JSON serializable" when using default + netconf plugin. + fragments: + - netconf-capabilites-fix.yaml + release_date: "2020-09-04" + 1.3.0: + changes: + bugfixes: + - cli_parse - Ensure only native types are returned to the control node from + the parser. + - netconf - Changed log level for message of using default netconf plugin to + match the level used when a platform-specific netconf plugin is found + minor_changes: + - Confirmed commit fails with TypeError in IOS XR netconf plugin (https://github.com/ansible-collections/cisco.iosxr/issues/74) + - The netconf_config module now allows root tag with namespace prefix. + - "cli_config: Add new return value diff which is returned when the cliconf + plugin supports onbox diff" + - "cli_config: Clarify when commands is returned when the module is run" + fragments: + - 134-cli-config-diff.yaml + - allow_root_tag_with_prefix.yaml + - cli_parse_fix.yaml + - iosxr_netconf_config_commit_testcase.yaml + - netconf-default.yaml + release_date: "2020-09-29" + 1.4.0: + changes: + bugfixes: + - Added support for private key based authentication with libssh transport (https://github.com/ansible-collections/ansible.netcommon/issues/168) + - Fixed ipaddr filter plugins in ansible.netcommon collections is not working + with latest Ansible (https://github.com/ansible-collections/ansible.netcommon/issues/157) + - Fixed netconf_rpc task fails due to encoding issue in the response (https://github.com/ansible-collections/ansible.netcommon/issues/151) + - Fixed ssh_type none issue while using net_put and net_get module (https://github.com/ansible-collections/ansible.netcommon/issues/153) + - Fixed unit tests under python3.5 + - 'ipaddr filter - query "address/prefix" (also: "gateway", "gw", "host/prefix", + "hostnet", and "router") now handles addresses with /32 prefix or /255.255.255.255 + netmask' + - network_cli - Update underlying ssh connection's play_context in update_play_context, + so that the username or password can be updated + minor_changes: + - "'prefix' added to NetworkTemplate class, inorder to handle the negate operation + for vyos config commands." + - Add support for json format input format for netconf modules using ``xmltodict`` + - Update docs for netconf_get and netconf_config examples using display=native + fragments: + - 135-network-cli-change-password.yaml + - 144-test-fixes.yaml + - 151-netconf_rpc_fix.yaml + - 153-part1-fix_ssh_type_none_issue.yaml + - 157-ipaddr-fix.yaml + - 168-libssh-privatekey-support.yaml + - ipaddr-host-prefix-32.yaml + - negate-command-vyos.yaml + - netconf_get_config_doc_updates.yaml + - netconf_xmltodict_support.yaml + release_date: "2020-10-29" + 1.4.1: + changes: + release_summary: + Change how black config is specified to avoid issues with Automation + Hub release process + fragments: + - revert_pyproject.yaml + release_date: "2020-10-29" + 1.5.0: + changes: + bugfixes: + - Add netconf_config integration tests for nxos (https://github.com/ansible-collections/ansible.netcommon/pull/185) + - Fix GetReply object has no attribute strip() (https://github.com/ansible-collections/cisco.iosxr/issues/97) + - Fix config diff logic if parent configuration is present more than once in + the candidate config and update docs (https://github.com/ansible-collections/ansible.netcommon/pull/189) + - Fix missing changed from net_get (https://github.com/ansible-collections/ansible.netcommon/issues/198) + - Fix netconf_config module integration test issuea (https://github.com/ansible-collections/ansible.netcommon/pull/177) + - Fix restconf_config incorrectly spoofs HTTP 409 codes (https://github.com/ansible-collections/ansible.netcommon/issues/191) + - Split checks for prompt and errors in network_cli so that detected errors + are not lost if the prompt is in a later chunk. + minor_changes: + - Add 'purged' to ACTION_STATES. + fragments: + - 177_netconf_config_test_issue.yaml + - 189_config_diff_fix.yaml + - 191_restconf_config_fix.yaml + - 198_net_get_missing_changed.yaml + - 97_getReplyobject_has_no_attribute_strip_issue.yaml + - add_purged_action_state.yaml + - error-independently.yaml + - netconf_nxos_tests.yaml + release_date: "2021-01-27" + 2.0.0: + changes: + breaking_changes: + - Removed vendored ipaddress package from collection. If you use ansible_collections.ansible.netcommon.plugins.module_utils.compat.ipaddress + in your collection, you will need to change this to import ipaddress instead. + If your content using ipaddress supports Python 2.7, you will additionally + need to make sure that the user has the ipaddress package installed. Please + refer to https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_best_practices.html#importing-and-using-shared-code + to see how to safely import external packages that may be missing from the + user's system A backport of ipaddress for Python 2.7 is available at https://pypi.org/project/ipaddress/ + bugfixes: + - Expose connection class object to rm_template (https://github.com/ansible-collections/ansible.netcommon/pull/180) + - network_cli - When using ssh_type libssh, handle closed connection gracefully + instead of throwing an exception + deprecated_features: + - Deprecate cli_parse module and textfsm, ttp, xml, json parser plugins as they + are moved to ansible.utils collection (https://github.com/ansible-collections/ansible.netcommon/pull/182 + https://github.com/ansible-collections/ansible.utils/pull/28) + major_changes: + - Remove deprecated connection arguments from netconf_config + minor_changes: + - Add SCP support when using ssh_type libssh + - Add `single_user_mode` option for command output caching. + - Move cli_config idempotent warning message with the task response under `warnings` + key if `changed` is `True` + - Reduce CPU usage and network module run time when using `ansible_network_import_modules` + - Support any() and all() filters in Jinja2. + fragments: + - 180_RMbase_engine.yaml + - 182-cli_parse_deprecate.yaml + - 212-update-documentation.yaml + - 213-docs-updates.yaml + - 217-pylibssh-conn-closed.yaml + - 226-libssh-scp.yaml + - 93-remove-ipaddress.yaml + - ansible_network_direct_execution.yaml + - config_module_warning_msg.yaml + - remove-netconf_config-args.yaml + - support_caching.yaml + - support_custom_filters.yaml + - update_requires_ansible.yaml + - yamllint.yaml + plugins: + cache: + - description: RAM backed, non persistent cache. + name: memory + namespace: null + release_date: "2021-03-01" + 2.0.1: + changes: + bugfixes: + - Allow setting `host_key_checking` through a play/task var for `network_cli`. + - Ensure passed-in terminal_initial_prompt and terminal_initial_answer values + are cast to bytes before using + - Update valid documentation for net_ping module. + - ncclient - catch and handle exception to prevent stack trace when running + in FIPS mode + - net_put - Remove temp file created when file already exist on destination + when mode is 'text'. + minor_changes: + - Several module_utils files were intended to be licensed BSD, but missing a + license preamble in the files. The preamble has been added, and all authors + for the files have given their assent to the intended license https://github.com/ansible-collections/ansible.netcommon/pull/122 + fragments: + - 100-bugfix-net-ping-docs.yaml + - 122-add-license.yaml + - 220-initial-prompt-bytes.yaml + - 227-remove_tests_sanity_requirements.yml + - 231-unit-tests.yaml + - 235-fix-net-put-issue.yaml + - 240-document-libssh-requirement.yaml + - fips-ncclient-import-error.yaml + - new_action_state.yaml + - no_log_fix.yaml + - set_host_key_checking.yaml + release_date: "2021-03-30" + 2.0.2: + changes: + bugfixes: + - Fix cli_parse issue with parsers in utils collection (https://github.com/ansible-collections/ansible.netcommon/pull/270) + - Support single_user_mode with Ansible 2.9. + fragments: + - 254-add_ignore_txt.yml + - cli_parse_fix.yaml + - single_user_mode.yaml + release_date: "2021-04-28" + 2.1.0: + changes: + bugfixes: + - Variables in play_context will now be updated for netconf connections on each + task run. + - fix SCP/SFTP when using network_cli with libssh + minor_changes: + - Add support for ProxyCommand with netconf connection. + fragments: + - 259-netconf-play-context.yaml + - drop-base-cache.yaml + - libssh-get-put.yaml + - support_proxycommand_netconf.yaml + release_date: "2021-05-17" + 2.2.0: + changes: + bugfixes: + - libssh - Fix fromatting of authenticity error message when not prompting for + input (https://github.com/ansible-collections/ansible.netcommon/issues/283) + - netconf - Fix connection with ncclient versions < 0.6.10 + - network_cli - Fix for execution failing when ansible_ssh_password is used + to specify password (https://github.com/ansible-collections/ansible.netcommon/issues/288) + minor_changes: + - Add variable to control ProxyCommand with libssh connection. + - "NetworkTemplate and ResouceModule base classes have been moved under module_utils.network.common.rm_base. + Stubs have been kept for backwards compatibility. These will be removed after + 2023-01-01. Please update imports for existing modules that subclass them. + The `cli_rm_builder `_ + has been updated to use the new imports. + + " + fragments: + - 257-libssh-proxy-var.yaml + - 288-netcli-password.yaml + - libssh-auth-msg.yaml + - ncclient-sock-arg.yaml + - update_rmbase.yaml + release_date: "2021-06-23" + 2.3.0: + changes: + minor_changes: + - Add vlan_expander filter + - Persistent connection options (persistent_command_timeout, persistent_log_messages, + etc.) have been unified across all persistent connections. New persistent + connections may also now get these options by extending the connection_persistent + documentation fragment. + fragments: + - 280-vlan_expander.yaml + - 295-connection-tests.yaml + - 308-unify-persistent.yaml + - fix_integration_test_iosxr_7.0.2.yaml + release_date: "2021-07-27" + 2.4.0: + changes: + bugfixes: + - network_cli - Add ability to set options inherited from paramiko/libssh in + ansible >= 2.11 (https://github.com/ansible-collections/ansible.netcommon/pull/271). + deprecated_features: + - network_cli - The paramiko_ssh setting ``look_for_keys`` was set automatically + based on the values of the ``password`` and ``private_key_file`` options passed + to network_cli. This option can now be set explicitly, and the automatic setting + of ``look_for_keys`` will be removed after 2024-01-01 (https://github.com/ansible-collections/ansible.netcommon/pull/271). + minor_changes: + - Add network_resource plugin to manage and provide single entry point for all + resource modules for higher oder roles. + fragments: + - 271-net-cli-options.yaml + - 318-netcli-tests.yaml + - backup-without-copy.yaml + - disable-look_for_keys-warning.yaml + - network_resource-version_added.yaml + - network_resource_plugin.yaml + modules: + - description: Manage resource modules + name: network_resource + namespace: "" + release_date: "2021-08-27" + 2.5.0: + changes: + bugfixes: + - network_cli - Provide clearer error message when a prompt regex fails to compile + - network_cli - fix issue when multiple terminal_initial_(prompt|answer) values + are given (https://github.com/ansible-collections/ansible.netcommon/issues/331). + minor_changes: + - Copied the cliconf, httpapi, netconf, and terminal base plugins and NetworkConnectionBase + into netcommon. These base plugins may now be imported from netcommmon instead + of ansible if a collection depends on netcommon versions newer than this version, + allowing features and bugfixes to flow to those collections without upgrading + ansible. + - Make ansible_network_os as optional param for httpapi connection plugin. + - Support removal of non-config lines from running config while taking backup. + - "`network_cli` - added new option 'become_errors' to determine how privilege + escalation failures are handled." + fragments: + - 0-copy_ignore_txt.yml + - 334-base-plugins.yaml + - httpapi_make_ansible_network_os_optional_param.yaml + - initial_prompt-bytes-fix.yaml + - non_config.yaml + - on_become_errors.yaml + - prompt-regex.yaml + release_date: "2021-12-07" + 2.5.1: + changes: + bugfixes: + - Fixed plugins inheriting from netcommon's base plugins (for example httpapi/restconf + or netconf/default) so that they can be properly loaded (https://github.com/ansible-collections/ansible.netcommon/issues/356). + fragments: + - 358-pluginloader.yaml + - pre-commit.yaml + release_date: "2022-02-09" + 2.6.0: + changes: + bugfixes: + - Fix issue with cli_parse native_parser plugin when input is empty (https://github.com/ansible-collections/ansible.netcommon/issues/347). + - No activity on the transport's channel was triggering a socket.timeout() after + 30 secs, even if persistent_command_timeout is set to a higher value. This + patch fixes it. + minor_changes: + - Redirected ipaddr filters to ansible.utils (https://github.com/ansible-collections/ansible.netcommon/pull/359). + - httpapi - new parameter retries in send() method limits the number of times + a request is retried when a HTTP error that can be worked around is encountered. + The default is to retry indefinitely to maintain old behavior, but this default + may change in a later breaking release. + fragments: + - 364-pre-commit-ci.yaml + - add_remove_prompt.yaml + - bugfix_cli_parse_native_parser.yaml + - deprecate_ipaddr_filters.yaml + - httpapi-retries.yaml + - shell_timeout.yaml + release_date: "2022-03-01" + 2.6.1: + changes: + bugfixes: + - Fix validate-module sanity test. + release_summary: Rereleased 2.6.0 with updated utils dependancy. + fragments: + - 2.6.0.yaml + - fix_sanity.yaml + release_date: "2022-03-10" + 3.0.0: + changes: + breaking_changes: + - httpapi - Change default value of ``import_modules`` option from ``no`` to + ``yes`` + - netconf - Change default value of ``import_modules`` option from ``no`` to + ``yes`` + - network_cli - Change default value of ``import_modules`` option from ``no`` + to ``yes`` + known_issues: + - "eos - When using eos modules on Ansible 2.9, tasks will occasionally fail + with ``import_modules`` enabled. This can be avoided by setting ``import_modules: + no``" + major_changes: + - cli_parse - this module has been moved to the ansible.utils collection. ``ansible.netcommon.cli_parse`` + will continue to work to reference the module in its new location, but this + redirect will be removed in a future release + - "network_cli - Change default value of `ssh_type` option from `paramiko` to + `auto`. This value will use libssh if the ansible-pylibssh module is installed, + otherwise will fallback to paramiko. + + " + fragments: + - 364-pre-commit-ci.yaml + - 384-cli_parse-move.yaml + - 387-change-defaults.yaml + - 390-sanity.yaml + - 394-change-defaults.yaml + - pre-commit-add-docs.yaml + - update-linter-config.yaml + release_date: "2022-04-26" + 3.0.1: + changes: + bugfixes: + - httpapi - Fix for improperly set hostname in url + - libssh - Fix for improperly set hostname in connect + - restconf - When non-JSON data is encountered, return the bytes found instead + of nothing. + fragments: + - 412-unit-updates.yaml + - 419-prettier.yaml + - 428.yaml + - 432.yaml + - fix-changelog-location.yaml + - import_modules-logging.yaml + - libssh-tests.yaml + - remote_addr.yaml + - restconf-not-json.yaml + - update-pre-commit.yaml + release_date: "2022-05-31" + 3.1.0: + changes: + minor_changes: + - Add grpc connection plugin support. + - Adds a new option `terminal_errors` in network_cli, that determines how terminal + setting failures are handled. + - libssh - Added `password_prompt` option to override default "password:" prompt + used by pylibssh + fragments: + - add-grpc-connection-plugin.yaml + - libssh-password-prompt.yaml + - terminal_errors.yaml + modules: + - description: Fetch configuration/state data from gRPC enabled target hosts. + name: grpc_config + namespace: "" + - description: Fetch configuration/state data from gRPC enabled target hosts. + name: grpc_get + namespace: "" + plugins: + connection: + - description: Provides a persistent connection using the gRPC protocol + name: grpc + namespace: null + release_date: "2022-08-02" + 3.1.1: + changes: + bugfixes: + - Fix a small number of potential use-before-assignment issues. + - Fix to set connection plugin options correctly. + - libssh - Removed the wording "Tech preview". From version 3.0.0 the default + if installed. + - libssh - add ssh_args, ssh_common_args, and ssh_extra_args options. These + options are exclusively for collecting proxy information from as an alternative + to the proxy_command option. + fragments: + - 441-pre-commit.yaml + - 448-set_options.yaml + - 451-libssh-remove-tech-preview.yaml + - 454-legacy_cleanup.yaml + - pylint.yaml + release_date: "2022-09-06" + 3.1.2: + changes: + bugfixes: + - libssh - check for minimum ansible-pylibssh version before using password_prompt + option. (https://github.com/ansible-collections/ansible.netcommon/pull/467) + fragments: + - 2.15-ignores.yaml + - libssh_check.yaml + release_date: "2022-09-30" + 3.1.3: + changes: + release_summary: + The v3.1.2 is unavailable on Ansible Automation Hub because + a technical issue. Please download and use v3.1.3 from Automation Hub. + fragments: + - prepare_312.yaml + release_date: "2022-10-04" + 4.0.0: + changes: + removed_features: + - napalm - Removed unused connection plugin. + - net_banner - Use _banner instead. + - net_interface - Use _interfaces instead. + - net_l2_interface - Use _l2_interfaces instead. + - net_l3_interface - Use _l3_interfaces instead. + - net_linkagg - Use _lag_interfaces instead. + - net_lldp - Use _lldp_global instead. + - net_lldp_interface - Use _lldp_interfaces instead. + - net_logging - Use _logging_global instead. + - net_static_route - Use _static_routes instead. + - net_system - Use _system instead. + - net_user - Use _user instead. + - net_vlan - Use _vlans instead. + - net_vrf - Use _vrf instead. + fragments: + - 2H22_removal.yaml + - license.yaml + release_date: "2022-10-13" + 4.1.0: + changes: + bugfixes: + - restconf_get - fix direction of XML deserialization when ``output == 'xml'`` + minor_changes: + - Add implementation for content_templates_parser. + fragments: + - add_content_template_parser.yaml + - fix_wrong_xml_direction.yaml + release_date: "2022-11-02" + 5.0.0: + changes: + breaking_changes: + - NetworkConnectionBase now inherits from PersistentConnectionBase in ansible.utils. + As a result, the minimum ansible.utils version has increased to 2.7.0. + - NetworkTemplate is no longer importable from ansible_collections.ansible.netcommon.plugins.module_utils.network.common + and should now be found at its proper location ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template + - ResourceModule is no longer importable from ansible_collections.ansible.netcommon.plugins.module_utils.network.common + and should now be found at its proper location ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module + - VALID_MASKS, is_masklen, is_netmask, to_bits, to_ipv6_network, to_masklen, + to_netmask, and to_subnet are no longer importable from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils + and should now be found at their proper location ansible.module_utils.common.network + bugfixes: + - Cast AnsibleUnsafeText to str in convert_doc_to_ansible_module_kwargs() to + keep CSafeLoader happy. This fixes issues with content scaffolding tools. + minor_changes: + - httpapi - Add option netcommon_httpapi_ciphers to allow overriding default + SSL/TLS ciphers. (https://github.com/ansible-collections/ansible.netcommon/pull/494) + removed_features: + - cli_parse - This plugin was moved to ansible.utils in version 1.0.0, and the + redirect to that collection has now been removed. + fragments: + - 23H1_breaking.yaml + - flake8.yaml + - netcommon_httpapi_ciphers.yaml + - persistentbase.yaml + - telnet.yaml + release_date: "2023-02-27" + 5.1.0: + changes: + bugfixes: + - httpapi - ``send()`` method no longer applied leftover kwargs to ``open_url()``. + Fix applies those arguments as intended (https://github.com/ansible-collections/ansible.netcommon/pull/524). + - network_cli - network cli connection avoids traceback when using invalid user + - network_cli - when receiving longer responses with libssh, parts of the response + were sometimes repeated. The response is now returned as it is received (https://github.com/ansible-collections/community.routeros/issues/132). + - network_resource - fix a potential UnboundLocalError if the module fails to + import a Resource Module. (https://github.com/ansible-collections/ansible.netcommon/pull/513) + - restconf - creation of new resources is no longer erroneously forced to use + POST. (https://github.com/ansible-collections/ansible.netcommon/issues/502) + minor_changes: + - libssh - add ``config_file`` option to specify an alternate SSH config file + to use. + - parse_cli - add support for multiple matches inside a block by adding new + dictionary key to result + - telnet - add ``stdout`` and ``stdout_lines`` to module output. + - telnet - add support for regexes to ``login_prompt`` and ``password_prompt``. + - telnet - apply ``timeout`` to command prompts. + fragments: + - 530-parse_cli.yaml + - httpapi-kwargs.yaml + - libssh-repeated-text.yaml + - libssh_config_file.yaml + - lint.yaml + - network_cli_bad_user.yaml + - restconf_put.yaml + - telnet-refactoring.yml + - ule-docs.yaml + release_date: "2023-04-03" + 5.1.1: + changes: + bugfixes: + - network_resource - do not append network_os to module names when building + supported resources list. This fix is only valid for cases where FACTS_RESOURCE_SUBSETS + is undefined. + fragments: + - resource_manager.yaml + release_date: "2023-05-09" + 5.1.2: + changes: + bugfixes: + - Ensure that all connection plugin options that should be strings are actually + strings (https://github.com/ansible-collections/ansible.netcommon/pull/549). + fragments: + - 549-connection-strings.yml + - 550-ansible-lint.yml + - gha_release.yaml + - line-length.yaml + release_date: "2023-07-05" + 5.1.3: + changes: + bugfixes: + - Vendor telnetlib from cpython (https://github.com/ansible-collections/ansible.netcommon/pull/546) + fragments: + - telnet.yaml + release_date: "2023-07-24" + 5.2.0: + changes: + bugfixes: + - Ensure that all connection plugin options that should be strings are actually + strings (https://github.com/ansible-collections/ansible.netcommon/pull/549). + deprecated_features: + - libssh - the ssh_*_args options are now marked that they will be removed after + 2026-01-01. + minor_changes: + - Add a new cliconf plugin ``default`` that can be used when no cliconf plugin + is found for a given network_os. This plugin only supports ``get()``. (https://github.com/ansible-collections/ansible.netcommon/pull/569) + - httpapi - Add additional option ``ca_path``, ``client_cert``, ``client_key``, + and ``http_agent`` that are available in open_url but not to httpapi. (https://github.com/ansible-collections/ansible.netcommon/issues/528) + - telnet - add crlf option to send CRLF instead of just LF (https://github.com/ansible-collections/ansible.netcommon/pull/440). + fragments: + - 440-telnet-add-crlf-option.yml + - 558-load_provider.yml + - default-cliconf.yaml + - httpapi_options.yaml + - ssh_args.yaml + - vlan_extender.yaml + plugins: + cliconf: + - description: General purpose cliconf plugin for new platforms + name: default + namespace: null + release_date: "2023-09-07" + 5.3.0: + changes: + bugfixes: + - Fix attribute types from string to str in filter plugins. + minor_changes: + - Add new module cli_backup that exclusively handles configuration backup. + fragments: + - cli_backup.yaml + - fix_attribute_type.yaml + - sanity_ignores.yaml + - trivial_lint.yaml + release_date: "2023-10-17" + 6.0.0: + changes: + major_changes: + - Bumping `requires_ansible` to `>=2.14.0`, since previous ansible-core versions + are EoL now. + release_summary: + Starting from this release, the minimum `ansible-core` version + this collection requires is `2.14.0`. That last known version compatible with + ansible-core<2.14 is `v5.3.0`. + fragments: + - major_600.yml + release_date: "2023-11-30" + 6.1.0: + changes: + bugfixes: + - libssh connection plugin - stop using deprecated ``PlayContext.verbosity`` + property that is no longer present in ansible-core 2.18 (https://github.com/ansible-collections/ansible.netcommon/pull/626). + - network_cli - removed deprecated play_context.verbosity property. + minor_changes: + - Add new module cli_restore that exclusively handles restoring of backup configuration + to target applaince. + fragments: + - 626-verbosity.yml + - add_cli_restore.yaml + - verbosity.yml + modules: + - description: Restore device configuration to network devices over network_cli + name: cli_restore + namespace: "" + release_date: "2024-04-11" + 6.1.1: + changes: + bugfixes: + - Added guidance for users to open an issue for the respective platform if plugin + support is needed. + - Improved module execution to gracefully handle cases where plugin support + is required, providing a clear error message to the user. + fragments: + - update_not_supported_exception.yaml + release_date: "2024-04-19" + 6.1.2: + changes: + doc_changes: + - Fixed module name and log consistency in parse_cli_textfsm filter doc. + fragments: + - 614-fix-parse_cli_textfsm-doc.yaml + release_date: "2024-05-22" + 6.1.3: + changes: + bugfixes: + - The v6.1.2 release introduced a change in cliconfbase's edit_config() signature + which broke many platform cliconfs. This patch release reverts that change. + fragments: + - bug_653.yaml + release_date: "2024-05-29" + 7.0.0: + changes: + bugfixes: + - Fix get api call during scp with libssh. + - Handle sftp error messages for file not present for routerOS. + known_issues: + - libssh - net_put and net_get fail when the destination file intended to be + fetched is not present. + major_changes: + - Bumping `requires_ansible` to `>=2.15.0`, since previous ansible-core versions + are EoL now. + release_summary: + Starting from this release, the minimum `ansible-core` version + this collection requires is `2.15.0`. The last known version compatible with + ansible-core<2.15 is v6.1.3. + fragments: + - fix-routeros-net_put.yaml + - libssh_get.yaml + - min_215.yaml + release_date: "2024-06-10" + 7.1.0: + changes: + bugfixes: + - Updated the error message for the content_templates parser to include the + correct parser name and detailed error information. + doc_changes: + - Add a simple regexp match example for multiple prompt with multiple answers. + This example could be used to for restarting a network device with a delay. + minor_changes: + - ansible.netcommon.persistent - Connection local is marked deprecated and all + dependent collections are advised to move to a proper connection plugin, complete + support of connection local will be removed in a release after 01-01-2027. + fragments: + - bye_connection_local.yaml + - cli-command-module.yaml + - readme_communication.yml + - update_error_msg.yaml + release_date: "2024-08-29" + 7.2.0: + changes: + bugfixes: + - libssh connection plugin - stop using long-deprecated and now removed internal + field from ansible-core's base connection plugin class (https://github.com/ansible-collections/ansible.netcommon/issues/522, + https://github.com/ansible-collections/ansible.netcommon/issues/690, https://github.com/ansible-collections/ansible.netcommon/pull/691). + deprecated_features: + - Added deprecation warnings for the above plugins, displayed when running respective + filter plugins. + - "`parse_cli_textfsm` filter plugin is deprecated and will be removed in a + future release after 2027-02-01. Use `ansible.utils.cli_parse` with the `ansible.utils.textfsm_parser` + parser as a replacement." + - "`parse_cli` filter plugin is deprecated and will be removed in a future release + after 2027-02-01. Use `ansible.utils.cli_parse` as a replacement." + - "`parse_xml` filter plugin is deprecated and will be removed in a future release + after 2027-02-01. Use `ansible.utils.cli_parse` with the `ansible.utils.xml_parser` + parser as a replacement." + doc_changes: + - Includes a new support related section in the README. + minor_changes: + - Exposes new libssh options to configure publickey_accepted_algorithms and + hostkeys. This requires ansible-pylibssh v1.1.0 or higher. + fragments: + - 691-libssh-connection.yml + - add_support_section.yaml + - deprecate_parsing_filter_plugins.yaml + - ignore_219.yaml + - libssh_pubkey_algo.yml + - update_bindep.yml + release_date: "2025-03-27" + 8.0.0: + changes: + major_changes: + - Bumping `requires_ansible` to `>=2.16.0`, since previous ansible-core versions + are EoL now. + release_summary: + With this release, the minimum required version of `ansible-core` + for this collection is `2.16.0`. The last version known to be compatible with + `ansible-core` versions below `2.16` is v7.2.0. + fragments: + - bump216.yml + release_date: "2025-04-11" + 8.0.1: + changes: + bugfixes: + - (#633) Fixed typo in ansible.netcommon.telnet parameter crlf (was clrf by + mistake) + - netconf - Adds check for netconf session_close RPC happens only if connection + is alive. + fragments: + - data_tag.yml + - fix_ci.yaml + - netconf_reset_connection.yaml + - telnet_crlf_parameter.yml + release_date: "2025-07-01" + 8.1.0: + changes: + bugfixes: + - Improved error handling in DirectExecutionModule._record_module_result method + for better compatibility with core<=2.18 + minor_changes: + - Changes to supplement direct execution of Ansible module in validate_config(utils.py) + and _patch_update_module(network.py) added. + - Override new 2.19.1+ AnsibleModule._record_module_result hook in network action + plugin to bypass module result serialization when direct execution is enabled + fragments: + - data_tagging_2_19.yaml + release_date: "2025-08-14" + 8.2.0: + changes: + bugfixes: + - Added support for private key passphrase in libssh connection plugin, when + using encrypted private keys specified by the C(ansible_private_key_file) + attribute. + - Avoid legacy imports deprecated in ansible-core 2.20 (https://github.com/ansible-collections/ansible.netcommon/pull/720). + - Avoid merging module_defaults for all ansible.netcommon.grpc_* modules. + - Set libssh logging level to DEBUG when Ansible verbosity is greater than 3, + to aid in troubleshooting connection issues. + minor_changes: + - Exposes new libssh option to configure key_exchange_algorithms. This requires + ansible-pylibssh v1.3.0 or higher. + fragments: + - 720-ansible-core-2.20.yml + - fix_721_private_key.yml + - fix_grpc_module_defaults.yaml + - fix_sanity_220_devel.yaml + - fix_typo_error.yaml + - key_exchange_algo.yml + release_date: "2025-11-06" + 8.2.1: + changes: + bugfixes: + - Adds backward compatibility of handling src attributes, functional consistency + with ansible-core >= 2.19 + - Adds deprecation warning for the jinja2 processing functionality for src attributes, + src attributes in collections would still support considering file path but + they would not process template files directly once the functionality is deprecated. + - It is suggested to use ansible.builtin.template module to process templates + and use the processed template path in src attributes. + fragments: + - backward_src_219.yml + - test-fixes.yml + release_date: "2026-01-13" + 8.3.0: + changes: + minor_changes: + - Option to use libssh as transport while using netconf, is added. + - The ssh-python module is needed, which will ensure libssh as transport for + netconf operations. When use_libssh is enabled. + fragments: + - libsssh-fix.yml + release_date: "2026-02-04" + 8.4.0: + changes: + minor_changes: + - Option to use libssh as transport while using netconf, is added. + - The ssh-python module is needed, which will ensure libssh as transport for + netconf operations. When use_libssh is enabled. + release_summary: Re-released 8.3.1 with features added in the last release. + fragments: + - re_release_832.yaml + release_date: "2026-02-04" + 8.5.0: + changes: + bugfixes: + - filter plugins - Add plugin_routing redirects for ``ipaddr``, ``ipv4``, and + ``ipv6`` to ``ansible.utils`` so short names work when ``ansible.netcommon`` + is in the play's collection list (https://github.com/ansible-collections/ansible.utils/issues/404). + - filter plugins - Convert filter arguments to native Python types before ``AnsibleArgSpecValidator`` + so filters work with Ansible 2.19+ lazy containers that cannot be deep-copied + (e.g. ``vlan_parser``, ``vlan_expander``, ``hash_salt``, ``type5_pw``, ``comp_type5``, + ``parse_cli``, ``parse_cli_textfsm``, ``parse_xml``, ``pop_ace``). + - 'libssh connection - Fixed test_libssh_put_file unit test so the put_file + code path (used by net_put, copy module, and other file transfer + over libssh) is properly tested in CI. The test now sets connection options + and mocks Session so put_file does not trigger a real connection + attempt with an unset host (was failing with "Hostname required").' + - network action plugin - Fall back when remove_internal_keys is not importable + from ansible.vars.clean (e.g. some ansible-core builds), so direct module + execution still cleans module return data. + - "network_cli - Fixed file transfer (net_put / net_get) when ssh_type=libssh. + For put_file, no longer call_connect_uncached() before delegating to the libssh connection the libssh + plugin's put_file() calls _connect() internally. + For fetch_file, call _connect() then fetch_file() for libssh instead of _connect_uncached(), + so connection caching and the correct flow are used. Paramiko branch unchanged (still uses _connect_uncached() + for scp/sftp)." + deprecated_features: + - network_cli - The in-collection paramiko support (used when ssh_type is paramiko) + is a compatibility layer for environments where ansible-core's paramiko connection + is no longer available. This layer is deprecated and will be removed in a + release after 2028-02-01. Migrate to ssh_type=libssh by installing the ansible-pylibssh + package. + minor_changes: + - The dependency on ansible-pylibssh (for ssh_type=libssh / network_cli) is + now ansible-pylibssh>=1.4.0 in requirements.txt, raised from the previous + >=0.2.0 requirement. Installations still on ansible-pylibssh 0.x or 1.x below + 1.4.0 must upgrade to use the libssh connection path with this collection + release. + - "libssh connection - When log_path is set (e.g. via ANSIBLE_LOG_PATH or log_path + in ansible.cfg), the plugin now routes ansible-pylibssh (libssh) + logs into the same Ansible log file. Log level is derived from display.verbosity + using Python standard logging: verbosity 0 -> WARNING, + 1-2 -> INFO, 3+ -> DEBUG. This allows SSH/libssh debug and trace output to + appear in the configured log file for troubleshooting without changing + ansible-pylibssh configuration manually." + - network_cli - The in-collection paramiko path supports the same host key policy + behavior (including host_key_auto_add and known_hosts handling) and persistent + connection caching as the previous ansible-core paramiko integration. + - network_cli - When ssh_type is set to paramiko, the connection plugin now + uses an in-collection paramiko implementation instead of loading ansible-core's + paramiko connection plugin. This allows network_cli to work with versions + of ansible-core, where the paramiko connection plugin was removed. + fragments: + - 404-ipaddr-filter-redirect.yml + - filters-convert-to-native-ansible-2.19.yml + - libssh-log-path-verbosity-logging.yml + - network-action-remove-internal-keys-compat.yaml + - paramiko_implicit_network_cli.yaml + - sanity-fix-222.yaml + release_date: "2026-04-13" diff --git a/collections/ansible_collections/ansible/netcommon/changelogs/config.yaml b/collections/ansible_collections/ansible/netcommon/changelogs/config.yaml new file mode 100644 index 0000000..87c0c80 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/changelogs/config.yaml @@ -0,0 +1,32 @@ +changelog_filename_template: ../CHANGELOG.rst +changelog_filename_version_depth: 0 +changes_file: changelog.yaml +changes_format: combined +keep_fragments: false +mention_ancestor: true +new_plugins_after_name: removed_features +notesdir: fragments +prelude_section_name: release_summary +prelude_section_title: Release Summary +flatmap: true +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 + - - doc_changes + - Documentation Changes +title: Ansible Netcommon Collection +trivial_section_name: trivial diff --git a/collections/ansible_collections/ansible/netcommon/changelogs/fragments/.keep b/collections/ansible_collections/ansible/netcommon/changelogs/fragments/.keep new file mode 100644 index 0000000..e69de29 diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_backup_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_backup_module.rst new file mode 100644 index 0000000..11e337f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_backup_module.rst @@ -0,0 +1,149 @@ +.. _ansible.netcommon.cli_backup_module: + + +**************************** +ansible.netcommon.cli_backup +**************************** + +**Back up device configuration from network devices over network_cli** + + +Version added: 4.2.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module provides platform agnostic way of backing up text based configuration from network devices over network_cli connection plugin. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ defaults + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
The defaults argument will influence how the running-config is collected from the device. When the value is set to true, the command used to collect the running-config is append with the all keyword. When the value is set to false, the command is issued without the all keyword.
+
+
+ dir_path + +
+ path +
+
+ +
This option provides the path ending with directory name in which the backup configuration file will be stored. If the directory does not exist it will be first created and the filename is either the value of filename or default filename as described in filename options description. If the path value is not given in that case a backup directory will be created in the current working directory and backup configuration will be copied in filename within backup directory.
+
+
+ filename + +
+ string +
+
+ +
The filename to be used to store the backup configuration. If the filename is not given it will be generated based on the hostname, current time and date in format defined by <hostname>_config.<current-date>@<current-time>
+
+
+ + +Notes +----- + +.. note:: + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: configurable backup path + ansible.netcommon.cli_backup: + filename: backup.cfg + dir_path: /home/user + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + +
KeyReturnedDescription
+
+ backup_path + +
+ string +
+
always +
The full path to the backup file
+
+
Sample:
+
/playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Katherine Case (@Qalthos) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_command_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_command_module.rst new file mode 100644 index 0000000..e2b2e24 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_command_module.rst @@ -0,0 +1,279 @@ +.. _ansible.netcommon.cli_command_module: + + +***************************** +ansible.netcommon.cli_command +***************************** + +**Run a cli command on cli-based network devices** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Sends a command to a network device and returns the result read from the device. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ answer + +
+ list + / elements=string +
+
+ +
The answer to reply with if prompt is matched. The value can be a single answer or a list of answer for multiple prompts. In case the command execution results in multiple prompts the sequence of the prompt and excepted answer should be in same order.
+
+
+ check_all + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
By default if any one of the prompts mentioned in prompt option is matched it won't check for other prompts. This boolean flag, that when set to True will check for all the prompts mentioned in prompt option in the given order. If the option is set to True all the prompts should be received from remote host if not it will result in timeout.
+
+
+ command + +
+ string + / required +
+
+ +
The command to send to the remote network device. The resulting output from the command is returned, unless sendonly is set.
+
+
+ newline + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes ←
  • +
+
+
The boolean value, that when set to false will send answer to the device without a trailing newline.
+
+
+ prompt + +
+ list + / elements=string +
+
+ +
A single regex pattern or a sequence of patterns to evaluate the expected prompt from command.
+
+
+ sendonly + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
The boolean value, that when set to true will send command to the device but not wait for a result.
+
+
+ + +Notes +----- + +.. note:: + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: run show version on remote devices + ansible.netcommon.cli_command: + command: show version + + - name: run command with json formatted output + ansible.netcommon.cli_command: + command: show version | json + + - name: run command expecting user confirmation + ansible.netcommon.cli_command: + command: commit replace + prompt: This commit will replace or remove the entire running configuration + answer: "yes" + + - name: run command expecting user confirmation + ansible.netcommon.cli_command: + command: show interface summary + prompt: Press any key to continue + answer: y + newline: false + + - name: run config mode command and handle prompt/answer + ansible.netcommon.cli_command: + command: "{{ item }}" + prompt: + - Exit with uncommitted changes + answer: y + loop: + - configure + - set system syslog file test any any + - exit + + - name: multiple prompt, multiple answer (mandatory check for all prompts) + ansible.netcommon.cli_command: + command: copy sftp sftp://user@host//user/test.img + check_all: true + prompt: + - Confirm download operation + - Password + - Do you want to change that to the standby image + answer: + - y + - + - y + + - name: Simple regexp match for multiple prompt, multiple answer(mandatory check for all prompts) + ansible.netcommon.cli_command: + command: reload in 5 + check_all: true + prompt: + - Save\? + - confirm + answer: + - n + - y + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ json + +
+ dictionary +
+
when the device response is valid JSON +
A dictionary representing a JSON-formatted response
+
+
Sample:
+
{ + "architecture": "i386", + "bootupTimestamp": 1532649700.56, + "modelName": "vEOS", + "version": "4.15.9M" + [...] + }
+
+
+ stdout + +
+ string +
+
when sendonly is false +
The response from the command
+
+
Sample:
+
Version: VyOS 1.1.7[...]
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Katherine Case (@Qalthos) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_config_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_config_module.rst new file mode 100644 index 0000000..b61682b --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_config_module.rst @@ -0,0 +1,418 @@ +.. _ansible.netcommon.cli_config_module: + + +**************************** +ansible.netcommon.cli_config +**************************** + +**Push text based configuration to network devices over network_cli** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module provides platform agnostic way of pushing text based configuration to network devices over network_cli connection plugin. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ backup + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
This argument will cause the module to create a full backup of the current running config from the remote device before any changes are made. If the backup_options value is not given, the backup file is written to the backup folder in the playbook root directory or role root directory, if playbook is part of an ansible role. If the directory does not exist, it is created.
+
+
+ backup_options + +
+ dictionary +
+
+ +
This is a dict object containing configurable options related to backup file path. The value of this option is read only when backup is set to yes, if backup is set to no this option will be silently ignored.
+
+
+ dir_path + +
+ path +
+
+ +
This option provides the path ending with directory name in which the backup configuration file will be stored. If the directory does not exist it will be first created and the filename is either the value of filename or default filename as described in filename options description. If the path value is not given in that case a backup directory will be created in the current working directory and backup configuration will be copied in filename within backup directory.
+
+
+ filename + +
+ string +
+
+ +
The filename to be used to store the backup configuration. If the filename is not given it will be generated based on the hostname, current time and date in format defined by <hostname>_config.<current-date>@<current-time>
+
+
+ commit + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+
The commit argument instructs the module to push the configuration to the device. This is mapped to module check mode.
+
+
+ commit_comment + +
+ string +
+
+ +
The commit_comment argument specifies a text string to be used when committing the configuration. If the commit argument is set to False, this argument is silently ignored. This argument is only valid for the platforms that support commit operation with comment.
+
+
+ config + +
+ string +
+
+ +
The config to be pushed to the network device. This argument is mutually exclusive with rollback and either one of the option should be given as input. To ensure idempotency and correct diff the configuration lines should be similar to how they appear if present in the running configuration on device including the indentation.
+
+
+ defaults + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
The defaults argument will influence how the running-config is collected from the device. When the value is set to true, the command used to collect the running-config is append with the all keyword. When the value is set to false, the command is issued without the all keyword.
+
+
+ diff_ignore_lines + +
+ list + / elements=string +
+
+ +
Use this argument to specify one or more lines that should be ignored during the diff. This is used for lines in the configuration that are automatically updated by the system. This argument takes a list of regular expressions or exact line matches. Note that this parameter will be ignored if the platform has onbox diff support.
+
+
+ diff_match + +
+ string +
+
+
    Choices: +
  • line
  • +
  • strict
  • +
  • exact
  • +
  • none
  • +
+
+
Instructs the module on the way to perform the matching of the set of commands against the current device config. If diff_match is set to line, commands are matched line by line. If diff_match is set to strict, command lines are matched with respect to position. If diff_match is set to exact, command lines must be an equal match. Finally, if diff_match is set to none, the module will not attempt to compare the source configuration with the running configuration on the remote device. Note that this parameter will be ignored if the platform has onbox diff support.
+
+
+ diff_replace + +
+ string +
+
+
    Choices: +
  • line
  • +
  • block
  • +
  • config
  • +
+
+
Instructs the module on the way to perform the configuration on the device. If the diff_replace argument is set to line then the modified lines are pushed to the device in configuration mode. If the argument is set to block then the entire command block is pushed to the device in configuration mode if any line is not correct. Note that this parameter will be ignored if the platform has onbox diff support.
+
+
+ multiline_delimiter + +
+ string +
+
+ +
This argument is used when pushing a multiline configuration element to the device. It specifies the character to use as the delimiting character. This only applies to the configuration action.
+
+
+ replace + +
+ string +
+
+ +
If the replace argument is set to yes, it will replace the entire running-config of the device with the config argument value. For devices that support replacing running configuration from file on device like NXOS/JUNOS, the replace argument takes path to the file on the device that will be used for replacing the entire running-config. The value of config option should be None for such devices. Nexus 9K devices only support replace. Use net_put or nxos_file_copy in case of NXOS module to copy the flat file to remote device and then use set the fullpath to this argument.
+
+
+ rollback + +
+ integer +
+
+ +
The rollback argument instructs the module to rollback the current configuration to the identifier specified in the argument. If the specified rollback identifier does not exist on the remote device, the module will fail. To rollback to the most recent commit, set the rollback argument to 0. This option is mutually exclusive with config.
+
+
+ + +Notes +----- + +.. note:: + - The commands will be returned only for platforms that do not support onbox diff. The ``--diff`` option with the playbook will return the difference in configuration for devices that has support for onbox diff + - To ensure idempotency and correct diff the configuration lines in the relevant module options should be similar to how they appear if present in the running configuration on device including the indentation. + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: configure device with config + ansible.netcommon.cli_config: + config: "{{ lookup('ansible.builtin.template', 'basic/config.j2') }}" + + - name: multiline config + ansible.netcommon.cli_config: + config: | + hostname foo + feature nxapi + + - name: configure device with config with defaults enabled + ansible.netcommon.cli_config: + config: "{{ lookup('ansible.builtin.template', 'basic/config.j2') }}" + defaults: "yes" + + - name: Use diff_match + ansible.netcommon.cli_config: + config: "{{ lookup('file', 'interface_config') }}" + diff_match: none + + - name: nxos replace config + ansible.netcommon.cli_config: + replace: bootflash:nxoscfg + + - name: junos replace config + ansible.netcommon.cli_config: + replace: /var/home/ansible/junos01.cfg + + - name: commit with comment + ansible.netcommon.cli_config: + config: set system host-name foo + commit_comment: this is a test + + - name: configurable backup path + ansible.netcommon.cli_config: + config: "{{ lookup('ansible.builtin.template', 'basic/config.j2') }}" + backup: "yes" + backup_options: + filename: backup.cfg + dir_path: /home/user + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ backup_path + +
+ string +
+
when backup is yes +
The full path to the backup file
+
+
Sample:
+
/playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34
+
+
+ commands + +
+ list +
+
When supports_generated_diff=True and supports_onbox_diff=False in the platform's cliconf plugin +
The set of commands that will be pushed to the remote device
+
+
Sample:
+
['interface Loopback999', 'no shutdown']
+
+
+ diff + +
+ string +
+
When supports_onbox_diff=True in the platform's cliconf plugin +
The diff generated on the device when the commands were applied
+
+
Sample:
+
--- system:/running-config + +++ session:/ansible_1599745461-session-config + @@ -4,7 +4,7 @@ + ! + transceiver qsfp default-mode 4x10G + ! + -hostname veos + +hostname veos3 + ! + spanning-tree mode mstp
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Trishna Guha (@trishnaguha) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_restore_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_restore_module.rst new file mode 100644 index 0000000..d13d87e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.cli_restore_module.rst @@ -0,0 +1,128 @@ +.. _ansible.netcommon.cli_restore_module: + + +***************************** +ansible.netcommon.cli_restore +***************************** + +**Restore device configuration to network devices over network_cli** + + +Version added: 6.1.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module provides platform agnostic way of restore text based configuration to network devices over network_cli connection plugin. +- The module uses the platforms `config replace` commands to restore backup configuration that is already copied over to the appliance. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ filename + +
+ string +
+
+ +
Filename of the backup file, present in the appliance where the restore operation is to be performed. Check appliance for the configuration backup file name.
+
+
+ path + +
+ string +
+
+ +
The location in the target appliance where the file containing the backup exists. The path and the filename together create the input to the config replace command,
+
For an IOSXE appliance the path pattern is flash://<filename>
+
+
+ + +Notes +----- + +.. note:: + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Restore IOS-XE configuration + ansible.netcommon.cli_restore: + filename: backupDday.cfg + path: flash:// + + # Command fired + # ------------- + # config replace flash://backupDday.cfg force + + # Task Output + # ----------- + # + # ok: [BATMON] => changed=false + # __restore__: |- + # The rollback configlet from the last pass is listed below: + # ******** + # !List of Rollback Commands: + # Building configuration... + # Current configuration : 3781 bytes + # end + # ******** + # + # + # Rollback aborted after 5 passes + # The following commands are failed to apply to the IOS image. + # ******** + # Building configuration... + # Current configuration : 3781 bytes + # ******** + # invocation: + # module_args: + # filename: backupDday.cfg + + + + +Status +------ + + +Authors +~~~~~~~ + +- Sagar Paul (@KB-perByte) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.comp_type5_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.comp_type5_filter.rst new file mode 100644 index 0000000..130b68a --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.comp_type5_filter.rst @@ -0,0 +1,152 @@ +.. _ansible.netcommon.comp_type5_filter: + + +**************************** +ansible.netcommon.comp_type5 +**************************** + +**The comp_type5 filter plugin.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- The filter confirms configuration idempotency on use of type5_pw. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ encrypted_password + +
+ string + / required +
+
+ + +
The encrypted text.
+
+
+ return_original + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+ +
Return the original text.
+
+
+ unencrypted_password + +
+ string + / required +
+
+ + +
The unencrypted text.
+
+
+ + +Notes +----- + +.. note:: + - The filter confirms configuration idempotency on use of type5_pw. + - Can be used to validate password post hashing username cisco secret 5 {{ ansible_ssh_pass | ansible.netcommon.comp_type5(encrypted, True) }} + + + +Examples +-------- + +.. code-block:: yaml + + # Using comp_type5 + + # playbook + + - name: Set the facts + ansible.builtin.set_fact: + unencrypted_password: "cisco@123" + encrypted_password: "$1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/" + + - name: Invoke comp_type5 + ansible.builtin.debug: + msg: "{{ unencrypted_password | ansible.netcommon.comp_type5(encrypted_password, False) }}" + + # Task Output + # ----------- + # + # TASK [Set the facts] + # ok: [35.155.113.92] => changed=false + # ansible_facts: + # encrypted_password: $1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/ + # unencrypted_password: cisco@123 + + # TASK [Invoke comp_type5] + # ok: [35.155.113.92] => + # msg: true + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ken Celenza (@itdependsnetworks) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.default_cliconf.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.default_cliconf.rst new file mode 100644 index 0000000..78e126a --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.default_cliconf.rst @@ -0,0 +1,43 @@ +.. _ansible.netcommon.default_cliconf: + + +************************* +ansible.netcommon.default +************************* + +**General purpose cliconf plugin for new platforms** + + +Version added: 5.2.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This plugin attemts to provide low level abstraction apis for sending and receiving CLI commands from arbitrary network devices. + + + + + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.default_netconf.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.default_netconf.rst new file mode 100644 index 0000000..d8a836a --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.default_netconf.rst @@ -0,0 +1,76 @@ +.. _ansible.netcommon.default_netconf: + + +************************* +ansible.netcommon.default +************************* + +**Use default netconf plugin to run standard netconf commands as per RFC** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This default plugin provides low level abstraction apis for sending and receiving netconf commands as per Netconf RFC specification. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ ncclient_device_handler + +
+ string +
+
+ Default:
"default"
+
+ +
Specifies the ncclient device handler name for network os that support default netconf implementation as per Netconf RFC specification. To identify the ncclient device handler name refer ncclient library documentation.
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.enable_become.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.enable_become.rst new file mode 100644 index 0000000..2c1fed0 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.enable_become.rst @@ -0,0 +1,90 @@ +.. _ansible.netcommon.enable_become: + + +************************ +ansible.netcommon.enable +************************ + +**Switch to elevated permissions on a network device** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This become plugins allows elevated permissions on a remote network device. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ become_pass + +
+ - +
+
+ +
ini entries: +

[enable_become_plugin]
password = VALUE

+
+
env:ANSIBLE_BECOME_PASS
+
env:ANSIBLE_ENABLE_PASS
+
var: ansible_become_password
+
var: ansible_become_pass
+
var: ansible_enable_pass
+
+
password
+
+
+ + +Notes +----- + +.. note:: + - enable is really implemented in the network connection handler and as such can only be used with network connections. + - This plugin ignores the 'become_exe' and 'become_user' settings as it uses an API and not an executable. + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_config_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_config_module.rst new file mode 100644 index 0000000..9e75180 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_config_module.rst @@ -0,0 +1,285 @@ +.. _ansible.netcommon.grpc_config_module: + + +***************************** +ansible.netcommon.grpc_config +***************************** + +**Fetch configuration/state data from gRPC enabled target hosts.** + + +Version added: 3.1.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- gRPC is a high performance, open-source universal RPC framework. +- This module allows the user to append configs to an existing configuration in a gRPC enabled devices. + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- grpcio +- protobuf + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ backup + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
This argument will cause the module to create a full backup of the current running-config from the remote device before any changes are made. If the backup_options value is not given, the backup file is written to the backup folder in the playbook root directory or role root directory, if playbook is part of an ansible role. If the directory does not exist, it is created.
+
+
+ backup_options + +
+ dictionary +
+
+ +
This is a dict object containing configurable options related to backup file path. The value of this option is read only when backup is set to yes, if backup is set to no this option will be silently ignored.
+
+
+ dir_path + +
+ path +
+
+ +
This option provides the path ending with directory name in which the backup configuration file will be stored. If the directory does not exist it will be first created and the filename is either the value of filename or default filename as described in filename options description. If the path value is not given in that case a backup directory will be created in the current working directory and backup configuration will be copied in filename within backup directory.
+
+
+ filename + +
+ string +
+
+ +
The filename to be used to store the backup configuration. If the filename is not given it will be generated based on the hostname, current time and date in format defined by <hostname>_config.<current-date>@<current-time>
+
+
+ config + +
+ string +
+
+ +
This option specifies the string which acts as a filter to restrict the portions of the data to be retrieved from the target host device. If this option is not specified the entire configuration or state data is returned in response provided it is supported by target host.
+
+
+ state + +
+ string +
+
+ +
action to be performed
+
+
+ + +Notes +----- + +.. note:: + - This module requires the gRPC system service be enabled on the target host being managed. + - This module supports the use of connection=connection=ansible.netcommon.grpc + - This module requires the value of 'ansible_network_os' or 'grpc_type' configuration option (refer ansible.netcommon.grpc connection plugin documentation) be defined as an inventory variable. + - Tested against iosxrv 9k version 6.1.2. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge static route config + ansible.netcommon.grpc_config: + config: + Cisco-IOS-XR-ip-static-cfg:router-static: + default-vrf: + address-family: + vrfipv4: + vrf-unicast: + vrf-prefixes: + vrf-prefix: + - prefix: "1.2.3.6" + prefix-length: 32 + vrf-route: + vrf-next-hop-table: + vrf-next-hop-next-hop-address: + - next-hop-address: "10.0.2.2" + state: merged + + - name: Merge bgp config + ansible.netcommon.grpc_config: + config: "{{ lookup('file', 'bgp.json') }}" + state: merged + + - name: Find diff + diff: true + ansible.netcommon.grpc_config: + config: "{{ lookup('file', 'bgp_start.yml') }}" + state: merged + + - name: Backup running config + ansible.netcommon.grpc_config: + backup: true + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ backup_path + +
+ string +
+
when backup is yes +
The full path to the backup file
+
+
Sample:
+
/playbooks/ansible/backup/config.2022-07-16@22:28:34
+
+
+ diff + +
+ dictionary +
+
when diff is enabled +
If --diff option in enabled while running, the before and after configuration change are returned as part of before and after key.
+
+
+
+ stdout + +
+ string +
+
error mesage, when failure happens. empty , when the operation is successful +
The raw string containing response object received from the gRPC server.
+
+
Sample:
+
...
+
+
+ stdout_lines + +
+ list +
+
always apart from low-level errors (such as action plugin) +
The value of stdout split into a list
+
+
Sample:
+
['...', '...']
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Gomathi Selvi S (@GomathiselviS) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_connection.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_connection.rst new file mode 100644 index 0000000..8e19857 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_connection.rst @@ -0,0 +1,371 @@ +.. _ansible.netcommon.grpc_connection: + + +********************** +ansible.netcommon.grpc +********************** + +**Provides a persistent connection using the gRPC protocol** + + +Version added: 3.1.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This connection plugin provides a connection to remote devices over gRPC and is typically used with devices for sending and receiving RPC calls over gRPC framework. +- Note this connection plugin requires the grpcio python library to be installed on the local Ansible controller. + + + +Requirements +------------ +The below requirements are needed on the local Ansible controller node that executes this connection. + +- grpcio +- protobuf + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ certificate_chain_file + +
+ string +
+
+ +
ini entries: +

[grpc_connection]
certificate_chain_file = VALUE

+
+
env:ANSIBLE_CERTIFICATE_CHAIN_FILE
+
var: ansible_certificate_chain_file
+
+
The PEM encoded certificate chain file used to create a SSL-enabled channel. If the value is None, no certificate chain is used.
+
+
+ grpc_type + +
+ - +
+
+ Default:
"no"
+
+
ini entries: +

[grpc_connection]
type = no

+
+
env:ANSIBLE_GRPC_CONNECTION_TYPE
+
var: ansible_grpc_connection_type
+
+
This option indicates the grpc type and it can be used in place of network_os. (example cisco.iosxr.iosxr)
+
+
+ host + +
+ string +
+
+ Default:
"inventory_hostname"
+
+
var: ansible_host
+
+
Specifies the remote device FQDN or IP address to establish the gRPC connection to.
+
+
+ import_modules + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[ansible_network]
import_modules = yes

+
+
env:ANSIBLE_NETWORK_IMPORT_MODULES
+
var: ansible_network_import_modules
+
+
Reduce CPU usage and network module execution time by enabling direct execution. Instead of the module being packaged and executed by the shell, it will be directly executed by the Ansible control node using the same python interpreter as the Ansible process. Note- Incompatible with asynchronous mode. Note- Python 3 and Ansible 2.9.16 or greater required. Note- With Ansible 2.9.x fully qualified modules names are required in tasks.
+
+
+ network_os + +
+ string +
+
+ +
var: ansible_network_os
+
+
Configures the device platform network operating system. This value is used to load a device specific grpc plugin to communicate with the remote device.
+
+
+ password + +
+ string +
+
+ +
var: ansible_password
+
var: ansible_ssh_pass
+
+
Configures the user password used to authenticate to the remote device when first establishing the gRPC connection.
+
+
+ persistent_command_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
command_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
+
var: ansible_command_timeout
+
+
Configures, in seconds, the amount of time to wait for a command to return from the remote device. If this timer is exceeded before the command returns, the connection plugin will raise an exception and close.
+
+
+ persistent_connect_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
connect_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
+
var: ansible_connect_timeout
+
+
Configures, in seconds, the amount of time to wait when trying to initially establish a persistent connection. If this value expires before the connection to the remote device is completed, the connection will fail.
+
+
+ persistent_log_messages + +
+ boolean +
+
+ Default:
"no"
+
+
ini entries: +

[persistent_connection]
log_messages = no

+
+
env:ANSIBLE_PERSISTENT_LOG_MESSAGES
+
var: ansible_persistent_log_messages
+
+
This flag will enable logging the command executed and response received from target device in the ansible log file. For this option to work 'log_path' ansible configuration option is required to be set to a file path with write access.
+
Be sure to fully understand the security implications of enabling this option as it could create a security vulnerability by logging sensitive information in log file.
+
+
+ port + +
+ integer +
+
+ +
ini entries: +

[defaults]
remote_port = VALUE

+
+
env:ANSIBLE_REMOTE_PORT
+
var: ansible_port
+
+
Specifies the port on the remote device that listens for connections when establishing the gRPC connection. If None only the host part will be used.
+
+
+ private_key_file + +
+ string +
+
+ +
ini entries: +

[grpc_connection]
private_key_file = VALUE

+
+
env:ANSIBLE_PRIVATE_KEY_FILE
+
var: ansible_private_key_file
+
+
The PEM encoded private key file used to authenticate to the remote device when first establishing the grpc connection.
+
+
+ remote_user + +
+ string +
+
+ +
ini entries: +

[defaults]
remote_user = VALUE

+
+
env:ANSIBLE_REMOTE_USER
+
var: ansible_user
+
+
The username used to authenticate to the remote device when the gRPC connection is first established. If the remote_user is not specified, the connection will use the username of the logged in user.
+
Can be configured from the CLI via the --user or -u options.
+
+
+ root_certificates_file + +
+ string +
+
+ +
ini entries: +

[grpc_connection]
root_certificates_file = VALUE

+
+
env:ANSIBLE_ROOT_CERTIFICATES_FILE
+
var: ansible_root_certificates_file
+
+
The PEM encoded root certificate file used to create a SSL-enabled channel, if the value is None it reads the root certificates from a default location chosen by gRPC at runtime.
+
+
+ ssl_target_name_override + +
+ string +
+
+ +
ini entries: +

[grpc_connection]
ssl_target_name_override = VALUE

+
+
env:ANSIBLE_GPRC_SSL_TARGET_NAME_OVERRIDE
+
var: ansible_grpc_ssl_target_name_override
+
+
The option overrides SSL target name used for SSL host name checking. The name used for SSL host name checking will be the target parameter (assuming that the secure channel is an SSL channel). If this parameter is specified and the underlying is not an SSL channel, it will just be ignored.
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_get_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_get_module.rst new file mode 100644 index 0000000..480b5db --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.grpc_get_module.rst @@ -0,0 +1,241 @@ +.. _ansible.netcommon.grpc_get_module: + + +************************** +ansible.netcommon.grpc_get +************************** + +**Fetch configuration/state data from gRPC enabled target hosts.** + + +Version added: 3.1.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- gRPC is a high performance, open-source universal RPC framework. +- This module allows the user to fetch configuration and state data from gRPC enabled devices. + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- grpcio +- protobuf + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ command + +
+ string +
+
+ +
The option specifies the command to be executed on the target host and return the response in result. This option is supported if the gRPC target host supports executing CLI command over the gRPC connection.
+
+
+ data_type + +
+ string +
+
+ +
The type of data that should be fetched from the target host. The value depends on the capability of the gRPC server running on target host. The values can be config, oper etc. based on what is supported by the gRPC server. By default it will return both configuration and operational state data in response.
+
+
+ display + +
+ string +
+
+ +
Encoding scheme to use when serializing output from the device. The encoding scheme value depends on the capability of the gRPC server running on the target host. The values can be json, text etc.
+
+
+ section + +
+ string +
+
+ +
This option specifies the string which acts as a filter to restrict the portions of the data to be retrieved from the target host device. If this option is not specified the entire configuration or state data is returned in response provided it is supported by target host.
+

aliases: filter
+
+
+ + +Notes +----- + +.. note:: + - This module requires the gRPC system service be enabled on the target host being managed. + - This module supports the use of connection=ansible.netcommon.grpc. + - This module requires the value of 'ansible_network_os or grpc_type' configuration option (refer ansible.netcommon.grpc connection plugin documentation) be defined as an inventory variable. + - Tested against iosxrv 9k version 6.1.2. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Get bgp configuration data + grpc_get: + section: + Cisco-IOS-XR-ip-static-cfg:router-static: + - null + - name: run cli command + grpc_get: + command: "show version" + display: text + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ output + +
+ list +
+
when the device response is valid JSON +
A dictionary representing a JSON-formatted response, if the response is a valid json string
+
+
Sample:
+
[{ + "Cisco-IOS-XR-ip-static-cfg:router-static": { + "default-vrf": { + "address-family": { + "vrfipv4": { + "vrf-unicast": { + "vrf-prefixes": { + "vrf-prefix": [ + { + "prefix": "0.0.0.0", + "prefix-length": 0, + "vrf-route": { + "vrf-next-hop-table": { + "vrf-next-hop-interface-name-next-hop-address": [ + { + "interface-name": "MgmtEth0/RP0/CPU0/0", + "next-hop-address": "10.0.2.2" + } + ] + } + } + } + ] + } + } + } + } + } + } + }]
+
+
+ stdout + +
+ string +
+
always apart from low-level errors (such as action plugin) +
The raw string containing configuration or state data received from the gRPC server.
+
+
Sample:
+
...
+
+
+ stdout_lines + +
+ list +
+
always apart from low-level errors (such as action plugin) +
The value of stdout split into a list
+
+
Sample:
+
['...', '...']
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ganesh Nalawade (@ganeshrn) +- Gomathi Selvi S (@GomathiselviS) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.hash_salt_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.hash_salt_filter.rst new file mode 100644 index 0000000..04e6db2 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.hash_salt_filter.rst @@ -0,0 +1,113 @@ +.. _ansible.netcommon.hash_salt_filter: + + +*************************** +ansible.netcommon.hash_salt +*************************** + +**The hash_salt filter plugin.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- The filter plugin produces the salt from a hashed password. +- Using the parameters below - ``password | ansible.netcommon.hash_salt(template.yml``) + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ password + +
+ string + / required +
+
+ + +
This source data on which hash_salt invokes.
+
For example password | ansible.netcommon.hash_salt, in this case password represents the hashed password.
+
+
+ + +Notes +----- + +.. note:: + - The filter plugin produces the salt from a hashed password. + + + +Examples +-------- + +.. code-block:: yaml + + # Using hash_salt + + # playbook + + - name: Set the facts + ansible.builtin.set_fact: + password: "$1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/" + + - name: Invoke hash_salt + ansible.builtin.debug: + msg: "{{ password | ansible.netcommon.hash_salt() }}" + + + # Task Output + # ----------- + # + # TASK [Set the facts] + # ok: [host] => changed=false + # ansible_facts: + # password: $1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/ + + # TASK [Invoke hash_salt] + # ok: [host] => + # msg: avs + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ken Celenza (@itdependsnetworks) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.httpapi_connection.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.httpapi_connection.rst new file mode 100644 index 0000000..eb8e5b4 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.httpapi_connection.rst @@ -0,0 +1,498 @@ +.. _ansible.netcommon.httpapi_connection: + + +************************* +ansible.netcommon.httpapi +************************* + +**Use httpapi to run command on network appliances** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This connection plugin provides a connection to remote devices over a HTTP(S)-based api. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ become + +
+ boolean +
+
+ Default:
"no"
+
+
ini entries: +

[privilege_escalation]
become = no

+
+
env:ANSIBLE_BECOME
+
var: ansible_become
+
+
The become option will instruct the CLI session to attempt privilege escalation on platforms that support it. Normally this means transitioning from user mode to enable mode in the CLI session. If become is set to True and the remote device does not support privilege escalation or the privilege has already been elevated, then this option is silently ignored.
+
Can be configured from the CLI via the --become or -b options.
+
+
+ become_method + +
+ string +
+
+ Default:
"sudo"
+
+
ini entries: +

[privilege_escalation]
become_method = sudo

+
+
env:ANSIBLE_BECOME_METHOD
+
var: ansible_become_method
+
+
This option allows the become method to be specified in for handling privilege escalation. Typically the become_method value is set to enable but could be defined as other values.
+
+
+ ca_path + +
+ path +
+
added in 5.2.0
+
+ +
var: ansible_httpapi_ca_path
+
+
Path to CA cert bundle to use.
+
+
+ ciphers + +
+ list + / elements=string +
+
added in 5.0.0
+
+ +
var: ansible_httpapi_ciphers
+
+
SSL/TLS Ciphers to use for requests
+
When a list is provided, all ciphers are joined in order with :
+
See the OpenSSL Cipher List Format for more details.
+
The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions.
+
This option will have no effect on ansible-core<2.14 but a warning will be emitted.
+
+
+ client_cert + +
+ - +
+
added in 5.2.0
+
+ +
var: ansible_httpapi_client_cert
+
+
PEM formatted certificate chain file to be used for SSL client authentication. This file can also include the key as well, and if the key is included, client_key is not required
+
+
+ client_key + +
+ - +
+
added in 5.2.0
+
+ +
var: ansible_httpapi_client_key
+
+
PEM formatted file that contains the private key to be used for SSL client authentication. If client_cert contains both the certificate and key, this option is not required.
+
+
+ host + +
+ string +
+
+ Default:
"inventory_hostname"
+
+
var: inventory_hostname
+
var: ansible_host
+
+
Specifies the remote device FQDN or IP address to establish the HTTP(S) connection to.
+
+
+ http_agent + +
+ - +
+
added in 5.2.0
+
+ +
var: ansible_httpapi_http_agent
+
+
User-Agent to use in the request.
+
+
+ import_modules + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[ansible_network]
import_modules = yes

+
+
env:ANSIBLE_NETWORK_IMPORT_MODULES
+
var: ansible_network_import_modules
+
+
Reduce CPU usage and network module execution time by enabling direct execution. Instead of the module being packaged and executed by the shell, it will be directly executed by the Ansible control node using the same python interpreter as the Ansible process. Note- Incompatible with asynchronous mode. Note- Python 3 and Ansible 2.9.16 or greater required. Note- With Ansible 2.9.x fully qualified modules names are required in tasks.
+
+
+ network_os + +
+ string +
+
+ +
var: ansible_network_os
+
+
Configures the device platform network operating system. This value is used to load the correct httpapi plugin to communicate with the remote device
+
+
+ password + +
+ string +
+
+ +
var: ansible_password
+
var: ansible_httpapi_pass
+
var: ansible_httpapi_password
+
+
Configures the user password used to authenticate to the remote device when needed for the device API.
+
+
+ persistent_command_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
command_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
+
var: ansible_command_timeout
+
+
Configures, in seconds, the amount of time to wait for a command to return from the remote device. If this timer is exceeded before the command returns, the connection plugin will raise an exception and close.
+
+
+ persistent_connect_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
connect_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
+
var: ansible_connect_timeout
+
+
Configures, in seconds, the amount of time to wait when trying to initially establish a persistent connection. If this value expires before the connection to the remote device is completed, the connection will fail.
+
+
+ persistent_log_messages + +
+ boolean +
+
+ Default:
"no"
+
+
ini entries: +

[persistent_connection]
log_messages = no

+
+
env:ANSIBLE_PERSISTENT_LOG_MESSAGES
+
var: ansible_persistent_log_messages
+
+
This flag will enable logging the command executed and response received from target device in the ansible log file. For this option to work 'log_path' ansible configuration option is required to be set to a file path with write access.
+
Be sure to fully understand the security implications of enabling this option as it could create a security vulnerability by logging sensitive information in log file.
+
+
+ platform_type + +
+ string +
+
+ +
env:ANSIBLE_PLATFORM_TYPE
+
var: ansible_platform_type
+
+
Set type of platform.
+
+
+ port + +
+ integer +
+
+ +
ini entries: +

[defaults]
remote_port = VALUE

+
+
env:ANSIBLE_REMOTE_PORT
+
var: ansible_httpapi_port
+
+
Specifies the port on the remote device that listens for connections when establishing the HTTP(S) connection.
+
When unspecified, will pick 80 or 443 based on the value of use_ssl.
+
+
+ remote_user + +
+ string +
+
+ +
ini entries: +

[defaults]
remote_user = VALUE

+
+
env:ANSIBLE_REMOTE_USER
+
var: ansible_user
+
+
The username used to authenticate to the remote device when the API connection is first established. If the remote_user is not specified, the connection will use the username of the logged in user.
+
Can be configured from the CLI via the --user or -u options.
+
+
+ session_key + +
+ dictionary +
+
+ +
var: ansible_httpapi_session_key
+
+
Configures the session key to be used to authenticate to the remote device when needed for the device API.
+
This should contain a dictionary representing the key name and value for the token.
+
When specified, password is ignored.
+
+
+ use_proxy + +
+ boolean +
+
+ Default:
"yes"
+
+
var: ansible_httpapi_use_proxy
+
+
Whether to use https_proxy for requests.
+
+
+ use_ssl + +
+ boolean +
+
+ Default:
"no"
+
+
var: ansible_httpapi_use_ssl
+
+
Whether to connect using SSL (HTTPS) or not (HTTP).
+
+
+ validate_certs + +
+ boolean +
+
+ Default:
"yes"
+
+
var: ansible_httpapi_validate_certs
+
+
Whether to validate SSL certificates
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.libssh_connection.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.libssh_connection.rst new file mode 100644 index 0000000..54e537f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.libssh_connection.rst @@ -0,0 +1,479 @@ +.. _ansible.netcommon.libssh_connection: + + +************************ +ansible.netcommon.libssh +************************ + +**Run tasks using libssh for ssh connection** + + +Version added: 1.1.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Use the ansible-pylibssh python bindings to connect to targets +- The python bindings use libssh C library (https://www.libssh.org/) to connect to targets +- This plugin borrows a lot of settings from the ssh plugin as they both cover the same protocol. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ config_file + +
+ path +
+
added in 5.1.0
+
+ +
ini entries: +

[libssh_connection]
config_file = VALUE

+
+
env:ANSIBLE_LIBSSH_CONFIG_FILE
+
var: ansible_libssh_config_file
+
+
Alternate SSH config file location
+
+
+ host_key_auto_add + +
+ boolean +
+
+ +
ini entries: +

[libssh_connection]
host_key_auto_add = VALUE

+
+
env:ANSIBLE_LIBSSH_HOST_KEY_AUTO_ADD
+
+
TODO: write it
+
+
+ host_key_checking + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[defaults]
host_key_checking = yes

+

[libssh_connection]
host_key_checking = yes

+
+
env:ANSIBLE_HOST_KEY_CHECKING
+
env:ANSIBLE_SSH_HOST_KEY_CHECKING
+
env:ANSIBLE_LIBSSH_HOST_KEY_CHECKING
+
var: ansible_host_key_checking
+
var: ansible_ssh_host_key_checking
+
var: ansible_libssh_host_key_checking
+
+
Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host
+
+
+ hostkeys + +
+ string +
+
+ Default:
""
+
+
ini entries: +

[libssh_connection]
hostkeys =

+
+
env:ANSIBLE_LIBSSH_HOSTKEYS
+
var: ansible_libssh_hostkeys
+
+
Set the preferred server host key types as a comma-separated list (e.g., ssh-rsa,ssh-dss,ecdh-sha2-nistp256).
+
+
+ key_exchange_algorithms + +
+ string +
+
+ +
ini entries: +

[libssh_connection]
key_exchange_algorithms = VALUE

+
+
env:ANSIBLE_LIBSSH_KEY_EXCHANGE_ALGORITHMS
+
var: ansible_libssh_key_exchange_algorithms
+
+
Set the key exchange method as a comma-separated list (e.g., "ecdh-sha2-nistp256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1").
+
The list can be prepended by +,-,^ which will append, remove or move to the beginning (prioritizing) of the default list respectively. Giving an empty list after + and ^ will cause error.
+
+
+ look_for_keys + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[libssh_connection]
look_for_keys = yes

+
+
env:ANSIBLE_LIBSSH_LOOK_FOR_KEYS
+
+
TODO: write it
+
+
+ password + +
+ string +
+
+ +
var: ansible_password
+
var: ansible_ssh_pass
+
var: ansible_ssh_password
+
var: ansible_libssh_pass
+
var: ansible_libssh_password
+
+
Secret used to either login the ssh server or as a passphrase for ssh keys that require it
+
Can be set from the CLI via the --ask-pass option.
+
+
+ password_prompt + +
+ string +
+
added in 3.1.0
+
+ +
var: ansible_libssh_password_prompt
+
+
Text to match when using keyboard-interactive authentication to determine if the prompt is for the password.
+
Requires ansible-pylibssh version >= 1.0.0
+
+
+ private_key_passphrase + +
+ string +
+
+ +
var: ansible_private_key_password
+
var: ansible_private_key_passphrase
+
+
Passphrase used to unlock the private key specified by the ansible_private_key_file attribute.
+
This is required if the private key is encrypted with a passphrase.
+
+
+ proxy_command + +
+ string +
+
+ Default:
""
+
+
ini entries: +

[libssh_connection]
proxy_command =

+
+
env:ANSIBLE_LIBSSH_PROXY_COMMAND
+
var: ansible_paramiko_proxy_command
+
var: ansible_libssh_proxy_command
+
+
Proxy information for running the connection via a jumphost.
+
Also this plugin will scan 'ssh_args', 'ssh_extra_args' and 'ssh_common_args' from the 'ssh' plugin settings for proxy information if set.
+
+
+ pty + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[libssh_connection]
pty = yes

+
+
env:ANSIBLE_LIBSSH_PTY
+
+
TODO: write it
+
+
+ publickey_accepted_algorithms + +
+ string +
+
+ Default:
""
+
+
ini entries: +

[libssh_connection]
publickey_algorithms =

+
+
env:ANSIBLE_LIBSSH_PUBLICKEY_ALGORITHMS
+
var: ansible_libssh_publickey_algorithms
+
+
List of algorithms to forward to SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES.
+
+
+ remote_addr + +
+ string +
+
+ Default:
"inventory_hostname"
+
+
var: inventory_hostname
+
var: ansible_host
+
var: ansible_ssh_host
+
var: ansible_libssh_host
+
+
Address of the remote target
+
+
+ remote_user + +
+ string +
+
+ +
ini entries: +

[defaults]
remote_user = VALUE

+

[libssh_connection]
remote_user = VALUE

+
+
env:ANSIBLE_REMOTE_USER
+
env:ANSIBLE_LIBSSH_REMOTE_USER
+
var: ansible_user
+
var: ansible_ssh_user
+
var: ansible_libssh_user
+
+
User to login/authenticate as
+
Can be set from the CLI via the --user or -u options.
+
+
+ ssh_args + +
+ string +
+
added in 3.2.0
+
+ +
ini entries: +

[ssh_connection]
ssh_args = VALUE

+
+
env:ANSIBLE_SSH_ARGS
+
var: ansible_ssh_args
+
+
Arguments to pass to all ssh CLI tools.
+
ProxyCommand is the only supported argument.
+
This option is deprecated in favor of proxy_command and will be removed in a release after 2026-01-01.
+
+
+ ssh_common_args + +
+ string +
+
added in 3.2.0
+
+ +
ini entries: +

[ssh_connection]
ssh_common_args = VALUE

+
+
env:ANSIBLE_SSH_COMMON_ARGS
+
var: ansible_ssh_common_args
+
+
Common extra arguments for all ssh CLI tools.
+
ProxyCommand is the only supported argument.
+
This option is deprecated in favor of proxy_command and will be removed in a release after 2026-01-01.
+
+
+ ssh_extra_args + +
+ string +
+
added in 3.2.0
+
+ +
ini entries: +

[ssh_connection]
ssh_extra_args = VALUE

+
+
env:ANSIBLE_SSH_EXTRA_ARGS
+
var: ansible_ssh_extra_args
+
+
Extra arguments exclusive to the 'ssh' CLI tool.
+
ProxyCommand is the only supported argument.
+
This option is deprecated in favor of proxy_command and will be removed in a release after 2026-01-01.
+
+
+ use_persistent_connections + +
+ boolean +
+
+ Default:
"no"
+
+
ini entries: +

[defaults]
use_persistent_connections = no

+
+
env:ANSIBLE_USE_PERSISTENT_CONNECTIONS
+
+
Toggles the use of persistence for connections
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_get_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_get_module.rst new file mode 100644 index 0000000..0b4a78d --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_get_module.rst @@ -0,0 +1,132 @@ +.. _ansible.netcommon.net_get_module: + + +************************* +ansible.netcommon.net_get +************************* + +**Copy a file from a network device to Ansible Controller** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module provides functionality to copy file from network device to ansible controller. + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- scp if using protocol=scp with paramiko + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ dest + +
+ - +
+
+ Default:
["Same filename as specified in I(src). The path will be playbook root or role root directory if playbook is part of a role."]
+
+
Specifies the destination file. The path to the destination file can either be the full path on the Ansible control host or a relative path from the playbook or role root directory.
+
+
+ protocol + +
+ - +
+
+
    Choices: +
  • scp ←
  • +
  • sftp
  • +
+
+
Protocol used to transfer file.
+
+
+ src + +
+ - + / required +
+
+ +
Specifies the source file. The path to the source file can either be the full path on the network device or a relative path as per path supported by destination network device.
+
+
+ + +Notes +----- + +.. note:: + - Some devices need specific configurations to be enabled before scp can work These configuration should be pre-configured before using this module e.g ios - ``ip scp server enable``. + - User privilege to do scp on network device should be pre-configured e.g. ios - need user privilege 15 by default for allowing scp. + - Default destination of source file. + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: copy file from the network device to Ansible controller + ansible.netcommon.net_get: + src: running_cfg_ios1.txt + + - name: copy file from ios to common location at /tmp + ansible.netcommon.net_get: + src: running_cfg_sw1.txt + dest: /tmp/ios1.txt + + + + +Status +------ + + +Authors +~~~~~~~ + +- Deepak Agrawal (@dagrawal) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_ping_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_ping_module.rst new file mode 100644 index 0000000..1b197fa --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_ping_module.rst @@ -0,0 +1,273 @@ +.. _ansible.netcommon.net_ping_module: + + +************************** +ansible.netcommon.net_ping +************************** + +**Tests reachability using ping from a network device** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Tests reachability using ping from network device to a remote destination. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ count + +
+ - +
+
+ Default:
5
+
+
Number of packets to send.
+
+
+ dest + +
+ - + / required +
+
+ +
The IP Address or hostname (resolvable by switch) of the remote node.
+
+
+ source + +
+ - +
+
+ +
The source IP Address.
+
+
+ state + +
+ - +
+
+
    Choices: +
  • absent
  • +
  • present ←
  • +
+
+
Determines if the expected result is success or fail.
+
+
+ vrf + +
+ - +
+
+ Default:
"default"
+
+
The VRF to use for forwarding.
+
+
+ + +Notes +----- + +.. note:: + - For targets running Python, use the :ref:`ansible.builtin.shell ` module along with ping command instead. + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Test reachability to 10.10.10.10 using default vrf + ansible.netcommon.net_ping: + dest: 10.10.10.10 + + - name: Test reachability to 10.20.20.20 using prod vrf + ansible.netcommon.net_ping: + dest: 10.20.20.20 + vrf: prod + + - name: Test unreachability to 10.30.30.30 using default vrf + ansible.netcommon.net_ping: + dest: 10.30.30.30 + state: absent + + - name: Test reachability to 10.40.40.40 using prod vrf and setting count and source + ansible.netcommon.net_ping: + dest: 10.40.40.40 + source: loopback0 + vrf: prod + count: 20 + + - Note: + - For targets running Python, use the M(ansible.builtin.shell) module along with ping command instead. + - Example: + name: ping + shell: ping -c 1 + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ commands + +
+ list +
+
always +
Show the command sent.
+
+
Sample:
+
['ping vrf prod 10.40.40.40 count 20 source loopback0']
+
+
+ packet_loss + +
+ string +
+
always +
Percentage of packets lost.
+
+
Sample:
+
0%
+
+
+ packets_rx + +
+ integer +
+
always +
Packets successfully received.
+
+
Sample:
+
20
+
+
+ packets_tx + +
+ integer +
+
always +
Packets successfully transmitted.
+
+
Sample:
+
20
+
+
+ rtt + +
+ dictionary +
+
always +
Show RTT stats.
+
+
Sample:
+
{'avg': 2, 'max': 8, 'min': 1}
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Jacob McGill (@jmcgill298) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_put_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_put_module.rst new file mode 100644 index 0000000..470e940 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.net_put_module.rst @@ -0,0 +1,152 @@ +.. _ansible.netcommon.net_put_module: + + +************************* +ansible.netcommon.net_put +************************* + +**Copy a file from Ansible Controller to a network device** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module provides functionality to copy file from Ansible controller to network devices. + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- scp if using protocol=scp with paramiko + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ dest + +
+ - +
+
+ Default:
["Filename from src and at default directory of user shell on network_os."]
+
+
Specifies the destination file. The path to destination file can either be the full path or relative path as supported by network_os.
+
+
+ mode + +
+ - +
+
+
    Choices: +
  • binary ←
  • +
  • text
  • +
+
+
Set the file transfer mode. If mode is set to text then src file will go through Jinja2 template engine to replace any vars if present in the src file. If mode is set to binary then file will be copied as it is to destination device.
+
+
+ protocol + +
+ - +
+
+
    Choices: +
  • scp ←
  • +
  • sftp
  • +
+
+
Protocol used to transfer file.
+
+
+ src + +
+ - + / required +
+
+ +
Specifies the source file. The path to the source file can either be the full path on the Ansible control host or a relative path from the playbook or role root directory.
+
+
+ + +Notes +----- + +.. note:: + - Some devices need specific configurations to be enabled before scp can work These configuration should be pre-configured before using this module e.g ios - ``ip scp server enable``. + - User privilege to do scp on network device should be pre-configured e.g. ios - need user privilege 15 by default for allowing scp. + - Default destination of source file. + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: copy file from ansible controller to a network device + ansible.netcommon.net_put: + src: running_cfg_ios1.txt + + - name: copy file at root dir of flash in slot 3 of sw1(ios) + ansible.netcommon.net_put: + src: running_cfg_sw1.txt + protocol: sftp + dest: flash3:/running_cfg_sw1.txt + + + + +Status +------ + + +Authors +~~~~~~~ + +- Deepak Agrawal (@dagrawal) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_config_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_config_module.rst new file mode 100644 index 0000000..f2c8366 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_config_module.rst @@ -0,0 +1,636 @@ +.. _ansible.netcommon.netconf_config_module: + + +******************************** +ansible.netcommon.netconf_config +******************************** + +**netconf device configuration** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Netconf is a network management protocol developed and standardized by the IETF. It is documented in RFC 6241. +- This module allows the user to send a configuration XML file to a netconf device, and detects if there was a configuration change. + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- ncclient + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ backup + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
This argument will cause the module to create a full backup of the current running-config from the remote device before any changes are made. If the backup_options value is not given, the backup file is written to the backup folder in the playbook root directory or role root directory, if playbook is part of an ansible role. If the directory does not exist, it is created.
+
+
+ backup_options + +
+ dictionary +
+
+ +
This is a dict object containing configurable options related to backup file path. The value of this option is read only when backup is set to yes, if backup is set to no this option will be silently ignored.
+
+
+ dir_path + +
+ path +
+
+ +
This option provides the path ending with directory name in which the backup configuration file will be stored. If the directory does not exist it will be first created and the filename is either the value of filename or default filename as described in filename options description. If the path value is not given in that case a backup directory will be created in the current working directory and backup configuration will be copied in filename within backup directory.
+
+
+ filename + +
+ string +
+
+ +
The filename to be used to store the backup configuration. If the filename is not given it will be generated based on the hostname, current time and date in format defined by <hostname>_config.<current-date>@<current-time>
+
+
+ commit + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes ←
  • +
+
+
This boolean flag controls if the configuration changes should be committed or not after editing the candidate datastore. This option is supported only if remote Netconf server supports :candidate capability. If the value is set to False commit won't be issued after edit-config operation and user needs to handle commit or discard-changes explicitly.
+
+
+ confirm + +
+ integer +
+
+ Default:
0
+
+
This argument will configure a timeout value for the commit to be confirmed before it is automatically rolled back. If the confirm_commit argument is set to False, this argument is silently ignored. If the value of this argument is set to 0, the commit is confirmed immediately. The remote host MUST support :candidate and :confirmed-commit capability for this option to .
+
+
+ confirm_commit + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
This argument will execute commit operation on remote device. It can be used to confirm a previous commit.
+
+
+ content + +
+ raw +
+
+ +
The configuration data as defined by the device's data models, the value can be either in xml string format or text format or python dictionary representation of JSON format.
+
In case of json string format it will be converted to the corresponding xml string using xmltodict library before pushing onto the remote host.
+
In case the value of this option isn text format the format should be supported by remote Netconf server.
+
If the value of content option is in xml format in that case the xml value should have config as root tag.
+

aliases: xml
+
+
+ default_operation + +
+ string +
+
+
    Choices: +
  • merge
  • +
  • replace
  • +
  • none
  • +
+
+
The default operation for <edit-config> rpc, valid values are merge, replace and none. If the default value is merge, the configuration data in the content option is merged at the corresponding level in the target datastore. If the value is replace the data in the content option completely replaces the configuration in the target datastore. If the value is none the target datastore is unaffected by the configuration in the config option, unless and until the incoming configuration data uses the operation operation to request a different operation.
+
+
+ delete + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
It instructs the module to delete the configuration from value mentioned in target datastore.
+
+
+ error_option + +
+ string +
+
+
    Choices: +
  • stop-on-error ←
  • +
  • continue-on-error
  • +
  • rollback-on-error
  • +
+
+
This option controls the netconf server action after an error occurs while editing the configuration.
+
If error_option=stop-on-error, abort the config edit on first error.
+
If error_option=continue-on-error, continue to process configuration data on error. The error is recorded and negative response is generated if any errors occur.
+
If error_option=rollback-on-error, rollback to the original configuration if any error occurs. This requires the remote Netconf server to support the error_option=rollback-on-error capability.
+
+
+ format + +
+ string +
+
+
    Choices: +
  • xml
  • +
  • text
  • +
  • json
  • +
+
+
The format of the configuration provided as value of content.
+
In case of json string format it will be converted to the corresponding xml string using xmltodict library before pushing onto the remote host.
+
In case of text format of the configuration should be supported by remote Netconf server.
+
If the value of format options is not given it tries to guess the data format of content option as one of xml or json or text.
+
If the data format is not identified it is set to xml by default.
+
+
+ get_filter + +
+ raw +
+
+ +
This argument specifies the XML string which acts as a filter to restrict the portions of the data retrieved from the remote device when comparing the before and after state of the device following calls to edit_config. When not specified, the entire configuration or state data is returned for comparison depending on the value of source option. The get_filter value can be either XML string or XPath or JSON string or native python dictionary, if the filter is in XPath format the NETCONF server running on remote host should support xpath capability else it will result in an error.
+
+
+ lock + +
+ string +
+
+
    Choices: +
  • never
  • +
  • always ←
  • +
  • if-supported
  • +
+
+
Instructs the module to explicitly lock the datastore specified as target. By setting the option value always is will explicitly lock the datastore mentioned in target option. It the value is never it will not lock the target datastore. The value if-supported lock the target datastore only if it is supported by the remote Netconf server.
+
+
+ save + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
The save argument instructs the module to save the configuration in target datastore to the startup-config if changed and if :startup capability is supported by Netconf server.
+
+
+ source_datastore + +
+ string +
+
+ +
Name of the configuration datastore to use as the source to copy the configuration to the datastore mentioned by target option. The values can be either running, candidate, startup or a remote URL
+

aliases: source
+
+
+ target + +
+ string +
+
+
    Choices: +
  • auto ←
  • +
  • candidate
  • +
  • running
  • +
+
+
Name of the configuration datastore to be edited. - auto, uses candidate and fallback to running - candidate, edit <candidate/> datastore and then commit - running, edit <running/> datastore directly
+

aliases: datastore
+
+
+ validate + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
This boolean flag if set validates the content of datastore given in target option. For this option to work remote Netconf server should support :validate capability.
+
+
+ + +Notes +----- + +.. note:: + - This module requires the netconf system service be enabled on the remote device being managed. + - This module supports devices with and without the candidate and confirmed-commit capabilities. It will always use the safer feature. + - This module supports the use of connection=netconf + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: use lookup filter to provide xml configuration + ansible.netcommon.netconf_config: + content: "{{ lookup('file', './config.xml') }}" + + - name: set ntp server in the device + ansible.netcommon.netconf_config: + content: | + + + + true + + ntp1 +
127.0.0.1
+
+
+
+
+ + - name: wipe ntp configuration + ansible.netcommon.netconf_config: + content: | + + + + false + + ntp1 + + + + + + - name: configure interface while providing different private key file path (for connection=netconf) + ansible.netcommon.netconf_config: + backup: true + register: backup_junos_location + vars: + ansible_private_key_file: /home/admin/.ssh/newprivatekeyfile + + - name: configurable backup path + ansible.netcommon.netconf_config: + backup: true + backup_options: + filename: backup.cfg + dir_path: /home/user + + - name: "configure using direct native format configuration (cisco iosxr)" + ansible.netcommon.netconf_config: + format: json + content: + { + "config": + { + "interface-configurations": + { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": + { + "active": "act", + "description": "test for ansible Loopback999", + "interface-name": "Loopback999", + }, + }, + }, + } + get_filter: + { + "interface-configurations": + { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": null, + }, + } + + - name: "configure using json string format configuration (cisco iosxr)" + ansible.netcommon.netconf_config: + format: json + content: | + { + "config": { + "interface-configurations": { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": { + "active": "act", + "description": "test for ansible Loopback999", + "interface-name": "Loopback999" + } + } + } + } + get_filter: | + { + "interface-configurations": { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": null + } + } + + # Make a round-trip interface description change, diff the before and after + # this demonstrates the use of the native display format and several utilities + # from the ansible.utils collection + + - name: Define the openconfig interface filter + set_fact: + filter: + interfaces: + "@xmlns": "http://openconfig.net/yang/interfaces" + interface: + name: Ethernet2 + + - name: Get the pre-change config using the filter + ansible.netcommon.netconf_get: + source: running + filter: "{{ filter }}" + display: native + register: pre + + - name: Update the description + ansible.utils.update_fact: + updates: + - path: pre.output.data.interfaces.interface.config.description + value: "Configured by ansible {{ 100 | random }}" + register: updated + + - name: Apply the new configuration + ansible.netcommon.netconf_config: + content: + config: + interfaces: "{{ updated.pre.output.data.interfaces }}" + + - name: Get the post-change config using the filter + ansible.netcommon.netconf_get: + source: running + filter: "{{ filter }}" + display: native + register: post + + - name: Show the differences between the pre and post configurations + ansible.utils.fact_diff: + before: "{{ pre.output.data|ansible.utils.to_paths }}" + after: "{{ post.output.data|ansible.utils.to_paths }}" + # TASK [Show the differences between the pre and post configurations] ******** + # --- before + # +++ after + # @@ -1,11 +1,11 @@ + # { + # - "@time-modified": "2020-10-23T12:27:17.462332477Z", + # + "@time-modified": "2020-10-23T12:27:21.744541708Z", + # "@xmlns": "urn:ietf:params:xml:ns:netconf:base:1.0", + # "interfaces.interface.aggregation.config['fallback-timeout']['#text']": "90", + # "interfaces.interface.aggregation.config['fallback-timeout']['@xmlns']": "http://arista.com/yang/openconfig/interfaces/augments", + # "interfaces.interface.aggregation.config['min-links']": "0", + # "interfaces.interface.aggregation['@xmlns']": "http://openconfig.net/yang/interfaces/aggregate", + # - "interfaces.interface.config.description": "Configured by ansible 56", + # + "interfaces.interface.config.description": "Configured by ansible 67", + # "interfaces.interface.config.enabled": "true", + # "interfaces.interface.config.mtu": "0", + # "interfaces.interface.config.name": "Ethernet2", + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ backup_path + +
+ string +
+
when backup is yes +
The full path to the backup file
+
+
Sample:
+
/playbooks/ansible/backup/config.2016-07-16@22:28:34
+
+
+ diff + +
+ dictionary +
+
when diff is enabled +
If --diff option in enabled while running, the before and after configuration change are returned as part of before and after key.
+
+
Sample:
+
{'after': '<rpc-reply> <data> <configuration> <version>17.3R1.10</version>...<--snip-->', 'before': '<rpc-reply> <data> <configuration> <version>17.3R1.10</version>...<--snip-->'}
+
+
+ server_capabilities + +
+ list +
+
success +
list of capabilities of the server
+
+
Sample:
+
['urn:ietf:params:netconf:base:1.1', 'urn:ietf:params:netconf:capability:confirmed-commit:1.0', 'urn:ietf:params:netconf:capability:candidate:1.0']
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Leandro Lisboa Penz (@lpenz) +- Ganesh Nalawade (@ganeshrn) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_connection.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_connection.rst new file mode 100644 index 0000000..e0c8d7b --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_connection.rst @@ -0,0 +1,410 @@ +.. _ansible.netcommon.netconf_connection: + + +************************* +ansible.netcommon.netconf +************************* + +**Provides a persistent connection using the netconf protocol** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This connection plugin provides a connection to remote devices over the SSH NETCONF subsystem. This connection plugin is typically used by network devices for sending and receiving RPC calls over NETCONF. +- Note this connection plugin requires ncclient to be installed on the local Ansible controller. + + + +Requirements +------------ +The below requirements are needed on the local Ansible controller node that executes this connection. + +- ncclient + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ host + +
+ string +
+
+ Default:
"inventory_hostname"
+
+
var: inventory_hostname
+
var: ansible_host
+
+
Specifies the remote device FQDN or IP address to establish the SSH connection to.
+
+
+ host_key_checking + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[defaults]
host_key_checking = yes

+

[paramiko_connection]
host_key_checking = yes

+
+
env:ANSIBLE_HOST_KEY_CHECKING
+
env:ANSIBLE_SSH_HOST_KEY_CHECKING
+
env:ANSIBLE_NETCONF_HOST_KEY_CHECKING
+
var: ansible_host_key_checking
+
var: ansible_ssh_host_key_checking
+
var: ansible_netconf_host_key_checking
+
+
Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host
+
+
+ import_modules + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[ansible_network]
import_modules = yes

+
+
env:ANSIBLE_NETWORK_IMPORT_MODULES
+
var: ansible_network_import_modules
+
+
Reduce CPU usage and network module execution time by enabling direct execution. Instead of the module being packaged and executed by the shell, it will be directly executed by the Ansible control node using the same python interpreter as the Ansible process. Note- Incompatible with asynchronous mode. Note- Python 3 and Ansible 2.9.16 or greater required. Note- With Ansible 2.9.x fully qualified modules names are required in tasks.
+
+
+ look_for_keys + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[paramiko_connection]
look_for_keys = yes

+
+
env:ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
+
+
Enables looking for ssh keys in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`).
+
This option is not supported when use_libssh=True,it will be ignored if use_libssh is enabled
+
+
+ netconf_ssh_config + +
+ string +
+
+ +
ini entries: +

[netconf_connection]
ssh_config = VALUE

+
+
env:ANSIBLE_NETCONF_SSH_CONFIG
+
var: ansible_netconf_ssh_config
+
+
This variable is used to enable bastion/jump host with netconf connection. If set to True the bastion/jump host ssh settings should be present in ~/.ssh/config file, alternatively it can be set to custom ssh configuration file path to read the bastion/jump host settings.
+
This option is not supported when use_libssh=True, it will be ignored if use_libssh is enabled
+
+
+ network_os + +
+ string +
+
+ +
var: ansible_network_os
+
+
Configures the device platform network operating system. This value is used to load a device specific netconf plugin. If this option is not configured (or set to auto), then Ansible will attempt to guess the correct network_os to use. If it can not guess a network_os correctly it will use default.
+
+
+ password + +
+ string +
+
+ +
var: ansible_password
+
var: ansible_ssh_pass
+
var: ansible_ssh_password
+
var: ansible_netconf_password
+
+
Configures the user password used to authenticate to the remote device when first establishing the SSH connection.
+
+
+ persistent_command_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
command_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
+
var: ansible_command_timeout
+
+
Configures, in seconds, the amount of time to wait for a command to return from the remote device. If this timer is exceeded before the command returns, the connection plugin will raise an exception and close.
+
+
+ persistent_connect_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
connect_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
+
var: ansible_connect_timeout
+
+
Configures, in seconds, the amount of time to wait when trying to initially establish a persistent connection. If this value expires before the connection to the remote device is completed, the connection will fail.
+
+
+ persistent_log_messages + +
+ boolean +
+
+ Default:
"no"
+
+
ini entries: +

[persistent_connection]
log_messages = no

+
+
env:ANSIBLE_PERSISTENT_LOG_MESSAGES
+
var: ansible_persistent_log_messages
+
+
This flag will enable logging the command executed and response received from target device in the ansible log file. For this option to work 'log_path' ansible configuration option is required to be set to a file path with write access.
+
Be sure to fully understand the security implications of enabling this option as it could create a security vulnerability by logging sensitive information in log file.
+
+
+ port + +
+ integer +
+
+ Default:
830
+
+
ini entries: +

[defaults]
remote_port = 830

+
+
env:ANSIBLE_REMOTE_PORT
+
var: ansible_port
+
+
Specifies the port on the remote device that listens for connections when establishing the SSH connection.
+
+
+ private_key_file + +
+ string +
+
+ +
ini entries: +

[defaults]
private_key_file = VALUE

+
+
env:ANSIBLE_PRIVATE_KEY_FILE
+
var: ansible_private_key_file
+
+
The private SSH key or certificate file used to authenticate to the remote device when first establishing the SSH connection.
+
+
+ proxy_command + +
+ string +
+
+ Default:
""
+
+
ini entries: +

[paramiko_connection]
proxy_command =

+
+
env:ANSIBLE_NETCONF_PROXY_COMMAND
+
var: ansible_paramiko_proxy_command
+
var: ansible_netconf_proxy_command
+
+
Proxy information for running the connection via a jumphost.
+
This requires ncclient >= 0.6.10 to be installed on the controller.
+
+
+ remote_user + +
+ string +
+
+ +
ini entries: +

[defaults]
remote_user = VALUE

+
+
env:ANSIBLE_REMOTE_USER
+
var: ansible_user
+
+
The username used to authenticate to the remote device when the SSH connection is first established. If the remote_user is not specified, the connection will use the username of the logged in user.
+
Can be configured from the CLI via the --user or -u options.
+
+
+ use_libssh + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
ini entries: +

[defaults]
netconf_libssh = no

+
+
env:ANSIBLE_NETCONF_LIBSSH
+
var: ansible_netconf_libssh
+
+
specifies whether to use libssh for netconf connection or not
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_get_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_get_module.rst new file mode 100644 index 0000000..69edcec --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_get_module.rst @@ -0,0 +1,378 @@ +.. _ansible.netcommon.netconf_get_module: + + +***************************** +ansible.netcommon.netconf_get +***************************** + +**Fetch configuration/state data from NETCONF enabled network devices.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- NETCONF is a network management protocol developed and standardized by the IETF. It is documented in RFC 6241. +- This module allows the user to fetch configuration and state data from NETCONF enabled network devices. + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- ncclient (>=v0.5.2) +- jxmlease (for display=json) +- xmltodict (for display=native) + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ display + +
+ string +
+
+
    Choices: +
  • json
  • +
  • pretty
  • +
  • xml
  • +
  • native
  • +
+
+
Encoding scheme to use when serializing output from the device. The option json will serialize the output as JSON data. If the option value is json it requires jxmlease to be installed on control node. The option pretty is similar to received XML response but is using human readable format (spaces, new lines). The option value xml is similar to received XML response but removes all XML namespaces.
+
+
+ filter + +
+ raw +
+
+ +
This argument specifies the string which acts as a filter to restrict the portions of the data to be are retrieved from the remote device. If this option is not specified entire configuration or state data is returned in result depending on the value of source option. The filter value can be either XML string or XPath or JSON string or native python dictionary, if the filter is in XPath format the NETCONF server running on remote host should support xpath capability else it will result in an error. If the filter is in JSON format the xmltodict library should be installed on the control node for JSON to XML conversion.
+
+
+ lock + +
+ string +
+
+
    Choices: +
  • never ←
  • +
  • always
  • +
  • if-supported
  • +
+
+
Instructs the module to explicitly lock the datastore specified as source. If no source is defined, the running datastore will be locked. By setting the option value always is will explicitly lock the datastore mentioned in source option. By setting the option value never it will not lock the source datastore. The value if-supported allows better interworking with NETCONF servers, which do not support the (un)lock operation for all supported datastores.
+
+
+ source + +
+ string +
+
+
    Choices: +
  • running
  • +
  • candidate
  • +
  • startup
  • +
+
+
This argument specifies the datastore from which configuration data should be fetched. Valid values are running, candidate and startup. If the source value is not set both configuration and state information are returned in response from running datastore.
+
+
+ + +Notes +----- + +.. note:: + - This module requires the NETCONF system service be enabled on the remote device being managed. + - This module supports the use of connection=netconf + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Get running configuration and state data + ansible.netcommon.netconf_get: + + - name: Get configuration and state data from startup datastore + ansible.netcommon.netconf_get: + source: startup + + - name: Get system configuration data from running datastore state (junos) + ansible.netcommon.netconf_get: + source: running + filter: + + - name: Get configuration and state data in JSON format + ansible.netcommon.netconf_get: + display: json + + - name: get schema list using subtree w/ namespaces + ansible.netcommon.netconf_get: + display: json + filter: + lock: never + + - name: get schema list using xpath + ansible.netcommon.netconf_get: + display: xml + filter: /netconf-state/schemas/schema + + - name: get interface configuration with filter (iosxr) + ansible.netcommon.netconf_get: + display: pretty + filter: + lock: if-supported + + - name: Get system configuration data from running datastore state (junos) + ansible.netcommon.netconf_get: + source: running + filter: + lock: if-supported + + - name: Get complete configuration data from running datastore (SROS) + ansible.netcommon.netconf_get: + source: running + filter: + + - name: Get complete state data (SROS) + ansible.netcommon.netconf_get: + filter: + + - name: "get configuration with json filter string and native output (using xmltodict)" + netconf_get: + filter: | + { + "interface-configurations": { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": null + } + } + display: native + + - name: Define the Cisco IOSXR interface filter + set_fact: + filter: + interface-configurations: + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg" + interface-configuration: null + + - name: "get configuration with native filter type using set_facts" + ansible.netcommon.netconf_get: + filter: "{{ filter }}" + display: native + register: result + + - name: "get configuration with direct native filter type" + ansible.netcommon.netconf_get: + filter: + { + "interface-configurations": + { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": null, + }, + } + display: native + register: result + + # Make a round-trip interface description change, diff the before and after + # this demonstrates the use of the native display format and several utilities + # from the ansible.utils collection + + - name: Define the openconfig interface filter + set_fact: + filter: + interfaces: + "@xmlns": "http://openconfig.net/yang/interfaces" + interface: + name: Ethernet2 + + - name: Get the pre-change config using the filter + ansible.netcommon.netconf_get: + source: running + filter: "{{ filter }}" + display: native + register: pre + + - name: Update the description + ansible.utils.update_fact: + updates: + - path: pre.output.data.interfaces.interface.config.description + value: "Configured by ansible {{ 100 | random }}" + register: updated + + - name: Apply the new configuration + ansible.netcommon.netconf_config: + content: + config: + interfaces: "{{ updated.pre.output.data.interfaces }}" + + - name: Get the post-change config using the filter + ansible.netcommon.netconf_get: + source: running + filter: "{{ filter }}" + display: native + register: post + + - name: Show the differences between the pre and post configurations + ansible.utils.fact_diff: + before: "{{ pre.output.data|ansible.utils.to_paths }}" + after: "{{ post.output.data|ansible.utils.to_paths }}" + # TASK [Show the differences between the pre and post configurations] ******** + # --- before + # +++ after + # @@ -1,11 +1,11 @@ + # { + # - "@time-modified": "2020-10-23T12:27:17.462332477Z", + # + "@time-modified": "2020-10-23T12:27:21.744541708Z", + # "@xmlns": "urn:ietf:params:xml:ns:netconf:base:1.0", + # "interfaces.interface.aggregation.config['fallback-timeout']['#text']": "90", + # "interfaces.interface.aggregation.config['fallback-timeout']['@xmlns']": "http://arista.com/yang/openconfig/interfaces/augments", + # "interfaces.interface.aggregation.config['min-links']": "0", + # "interfaces.interface.aggregation['@xmlns']": "http://openconfig.net/yang/interfaces/aggregate", + # - "interfaces.interface.config.description": "Configured by ansible 56", + # + "interfaces.interface.config.description": "Configured by ansible 67", + # "interfaces.interface.config.enabled": "true", + # "interfaces.interface.config.mtu": "0", + # "interfaces.interface.config.name": "Ethernet2", + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ output + +
+ complex +
+
If the display format is selected as json it is returned as dict type and the conversion is done using jxmlease python library. If the display format is selected as native it is returned as dict type and the conversion is done using xmltodict python library. If the display format is xml or pretty it is returned as a string apart from low-level errors (such as action plugin). +
Based on the value of display option will return either the set of transformed XML to JSON format from the RPC response with type dict or pretty XML string response (human-readable) or response with namespace removed from XML string.
+
+
  +
+ formatted_output + +
+ string +
+
+
Contains formatted response received from remote host as per the value in display format.
+
+
+
+ stdout + +
+ string +
+
always apart from low-level errors (such as action plugin) +
The raw XML string containing configuration or state data received from the underlying ncclient library.
+
+
Sample:
+
...
+
+
+ stdout_lines + +
+ list +
+
always apart from low-level errors (such as action plugin) +
The value of stdout split into a list
+
+
Sample:
+
['...', '...']
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ganesh Nalawade (@ganeshrn) +- Sven Wisotzky (@wisotzky) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_rpc_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_rpc_module.rst new file mode 100644 index 0000000..ace4971 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.netconf_rpc_module.rst @@ -0,0 +1,276 @@ +.. _ansible.netcommon.netconf_rpc_module: + + +***************************** +ansible.netcommon.netconf_rpc +***************************** + +**Execute operations on NETCONF enabled network devices.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- NETCONF is a network management protocol developed and standardized by the IETF. It is documented in RFC 6241. +- This module allows the user to execute NETCONF RPC requests as defined by IETF RFC standards as well as proprietary requests. + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- ncclient (>=v0.5.2) +- jxmlease + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ content + +
+ string +
+
+ +
This argument specifies the optional request content (all RPC attributes). The content value can either be provided as XML formatted string or as dictionary.
+
+
+ display + +
+ string +
+
+
    Choices: +
  • json
  • +
  • pretty
  • +
  • xml
  • +
+
+
Encoding scheme to use when serializing output from the device. The option json will serialize the output as JSON data. If the option value is json it requires jxmlease to be installed on control node. The option pretty is similar to received XML response but is using human readable format (spaces, new lines). The option value xml is similar to received XML response but removes all XML namespaces.
+
+
+ rpc + +
+ string + / required +
+
+ +
This argument specifies the request (name of the operation) to be executed on the remote NETCONF enabled device.
+
+
+ xmlns + +
+ string +
+
+ +
NETCONF operations not defined in rfc6241 typically require the appropriate XML namespace to be set. In the case the request option is not already provided in XML format, the namespace can be defined by the xmlns option.
+
+
+ + +Notes +----- + +.. note:: + - This module requires the NETCONF system service be enabled on the remote device being managed. + - This module supports the use of connection=netconf + - To execute ``get-config``, ``get`` or ``edit-config`` requests it is recommended to use the Ansible *netconf_get* and *netconf_config* modules. + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: lock candidate + ansible.netcommon.netconf_rpc: + rpc: lock + content: + target: + candidate: + + - name: unlock candidate + ansible.netcommon.netconf_rpc: + rpc: unlock + xmlns: urn:ietf:params:xml:ns:netconf:base:1.0 + content: "{'target': {'candidate': None}}" + + - name: discard changes + ansible.netcommon.netconf_rpc: + rpc: discard-changes + + - name: get-schema + ansible.netcommon.netconf_rpc: + rpc: get-schema + xmlns: urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring + content: + identifier: ietf-netconf + version: '2011-06-01' + + - name: copy running to startup + ansible.netcommon.netconf_rpc: + rpc: copy-config + content: + source: + running: + target: + startup: + + - name: get schema list with JSON output + ansible.netcommon.netconf_rpc: + rpc: get + content: | + + + + + + display: json + + - name: get schema using XML request + ansible.netcommon.netconf_rpc: + rpc: get-schema + xmlns: urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring + content: | + ietf-netconf-monitoring + 2010-10-04 + display: json + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ output + +
+ complex +
+
when the display format is selected as JSON it is returned as dict type, if the display format is xml or pretty pretty it is returned as a string apart from low-level errors (such as action plugin). +
Based on the value of display option will return either the set of transformed XML to JSON format from the RPC response with type dict or pretty XML string response (human-readable) or response with namespace removed from XML string.
+
+
  +
+ formatted_output + +
+ string +
+
+
Contains formatted response received from remote host as per the value in display format.
+
+
+
+ stdout + +
+ string +
+
always apart from low-level errors (such as action plugin) +
The raw XML string containing configuration or state data received from the underlying ncclient library.
+
+
Sample:
+
...
+
+
+ stdout_lines + +
+ list +
+
always apart from low-level errors (such as action plugin) +
The value of stdout split into a list
+
+
Sample:
+
['...', '...']
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ganesh Nalawade (@ganeshrn) +- Sven Wisotzky (@wisotzky) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.network_cli_connection.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.network_cli_connection.rst new file mode 100644 index 0000000..42ee3b4 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.network_cli_connection.rst @@ -0,0 +1,641 @@ +.. _ansible.netcommon.network_cli_connection: + + +***************************** +ansible.netcommon.network_cli +***************************** + +**Use network_cli to run command on network appliances** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This connection plugin provides a connection to remote devices over the SSH and implements a CLI shell. This connection plugin is typically used by network devices for sending and receiving CLi commands to network devices. + + + +Requirements +------------ +The below requirements are needed on the local Ansible controller node that executes this connection. + +- ansible-pylibssh if using *ssh_type=libssh* + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ become + +
+ boolean +
+
+ Default:
"no"
+
+
ini entries: +

[privilege_escalation]
become = no

+
+
env:ANSIBLE_BECOME
+
var: ansible_become
+
+
The become option will instruct the CLI session to attempt privilege escalation on platforms that support it. Normally this means transitioning from user mode to enable mode in the CLI session. If become is set to True and the remote device does not support privilege escalation or the privilege has already been elevated, then this option is silently ignored.
+
Can be configured from the CLI via the --become or -b options.
+
+
+ become_errors + +
+ string +
+
+
    Choices: +
  • ignore
  • +
  • warn
  • +
  • fail ←
  • +
+
+
var: ansible_network_become_errors
+
+
This option determines how privilege escalation failures are handled when become is enabled.
+
When set to ignore, the errors are silently ignored. When set to warn, a warning message is displayed. The default option fail, triggers a failure and halts execution.
+
+
+ become_method + +
+ string +
+
+ Default:
"sudo"
+
+
ini entries: +

[privilege_escalation]
become_method = sudo

+
+
env:ANSIBLE_BECOME_METHOD
+
var: ansible_become_method
+
+
This option allows the become method to be specified in for handling privilege escalation. Typically the become_method value is set to enable but could be defined as other values.
+
+
+ host + +
+ string +
+
+ Default:
"inventory_hostname"
+
+
var: inventory_hostname
+
var: ansible_host
+
+
Specifies the remote device FQDN or IP address to establish the SSH connection to.
+
+
+ host_key_auto_add + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[paramiko_connection]
host_key_auto_add = yes

+
+
env:ANSIBLE_HOST_KEY_AUTO_ADD
+
+
When true, unknown host keys are automatically added to the user known_hosts file and saved on connection close, so the first connection to a new host works without manual intervention (e.g. no need to run ssh-keyscan or set config).
+
When false, Ansible would prompt before adding; since network_cli uses persistent connections (background processes), the user cannot respond, so connection fails for new hosts unless the key is already in known_hosts.
+
Set to false if you need to avoid automatically trusting new host keys (e.g. on production systems where you manage known_hosts separately).
+
+
+ host_key_checking + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[defaults]
host_key_checking = yes

+

[persistent_connection]
host_key_checking = yes

+
+
env:ANSIBLE_HOST_KEY_CHECKING
+
env:ANSIBLE_SSH_HOST_KEY_CHECKING
+
var: ansible_host_key_checking
+
var: ansible_ssh_host_key_checking
+
+
Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host
+
+
+ import_modules + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[ansible_network]
import_modules = yes

+
+
env:ANSIBLE_NETWORK_IMPORT_MODULES
+
var: ansible_network_import_modules
+
+
Reduce CPU usage and network module execution time by enabling direct execution. Instead of the module being packaged and executed by the shell, it will be directly executed by the Ansible control node using the same python interpreter as the Ansible process. Note- Incompatible with asynchronous mode. Note- Python 3 and Ansible 2.9.16 or greater required. Note- With Ansible 2.9.x fully qualified modules names are required in tasks.
+
+
+ network_cli_retries + +
+ integer +
+
+ Default:
3
+
+
ini entries: +

[persistent_connection]
network_cli_retries = 3

+
+
env:ANSIBLE_NETWORK_CLI_RETRIES
+
var: ansible_network_cli_retries
+
+
Number of attempts to connect to remote host. The delay time between the retires increases after every attempt by power of 2 in seconds till either the maximum attempts are exhausted or any of the persistent_command_timeout or persistent_connect_timeout timers are triggered.
+
+
+ network_os + +
+ string +
+
+ +
var: ansible_network_os
+
+
Configures the device platform network operating system. This value is used to load the correct terminal and cliconf plugins to communicate with the remote device.
+
+
+ password + +
+ string +
+
+ +
var: ansible_password
+
var: ansible_ssh_pass
+
var: ansible_ssh_password
+
+
Configures the user password used to authenticate to the remote device when first establishing the SSH connection.
+
+
+ persistent_buffer_read_timeout + +
+ float +
+
+ Default:
0.1
+
+
ini entries: +

[persistent_connection]
buffer_read_timeout = 0.1

+
+
env:ANSIBLE_PERSISTENT_BUFFER_READ_TIMEOUT
+
var: ansible_buffer_read_timeout
+
+
Configures, in seconds, the amount of time to wait for the data to be read from Paramiko channel after the command prompt is matched. This timeout value ensures that command prompt matched is correct and there is no more data left to be received from remote host.
+
+
+ persistent_command_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
command_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
+
var: ansible_command_timeout
+
+
Configures, in seconds, the amount of time to wait for a command to return from the remote device. If this timer is exceeded before the command returns, the connection plugin will raise an exception and close.
+
+
+ persistent_connect_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
connect_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
+
var: ansible_connect_timeout
+
+
Configures, in seconds, the amount of time to wait when trying to initially establish a persistent connection. If this value expires before the connection to the remote device is completed, the connection will fail.
+
+
+ persistent_log_messages + +
+ boolean +
+
+ Default:
"no"
+
+
ini entries: +

[persistent_connection]
log_messages = no

+
+
env:ANSIBLE_PERSISTENT_LOG_MESSAGES
+
var: ansible_persistent_log_messages
+
+
This flag will enable logging the command executed and response received from target device in the ansible log file. For this option to work 'log_path' ansible configuration option is required to be set to a file path with write access.
+
Be sure to fully understand the security implications of enabling this option as it could create a security vulnerability by logging sensitive information in log file.
+
+
+ port + +
+ integer +
+
+ Default:
22
+
+
ini entries: +

[defaults]
remote_port = 22

+
+
env:ANSIBLE_REMOTE_PORT
+
var: ansible_port
+
+
Specifies the port on the remote device that listens for connections when establishing the SSH connection.
+
+
+ private_key_file + +
+ string +
+
+ +
ini entries: +

[defaults]
private_key_file = VALUE

+
+
env:ANSIBLE_PRIVATE_KEY_FILE
+
var: ansible_private_key_file
+
+
The private SSH key or certificate file used to authenticate to the remote device when first establishing the SSH connection.
+
+
+ remote_user + +
+ string +
+
+ +
ini entries: +

[defaults]
remote_user = VALUE

+
+
env:ANSIBLE_REMOTE_USER
+
var: ansible_user
+
+
The username used to authenticate to the remote device when the SSH connection is first established. If the remote_user is not specified, the connection will use the username of the logged in user.
+
Can be configured from the CLI via the --user or -u options.
+
+
+ single_user_mode + +
+ boolean +
+
added in 2.0.0
+
+ Default:
"no"
+
+
env:ANSIBLE_NETWORK_SINGLE_USER_MODE
+
var: ansible_network_single_user_mode
+
+
This option enables caching of data fetched from the target for re-use. The cache is invalidated when the target device enters configuration mode.
+
Applicable only for platforms where this has been implemented.
+
+
+ ssh_type + +
+ string +
+
+
    Choices: +
  • libssh
  • +
  • paramiko
  • +
  • auto ←
  • +
+
+
ini entries: +

[persistent_connection]
ssh_type = auto

+
+
env:ANSIBLE_NETWORK_CLI_SSH_TYPE
+
var: ansible_network_cli_ssh_type
+
+
The python package that will be used by the network_cli connection plugin to create a SSH connection to remote host.
+
libssh will use the ansible-pylibssh package, which needs to be installed in order to work.
+
paramiko will instead use the paramiko package to manage the SSH connection. This option is deprecated and will be removed in a release after 2028-02-01. Migrate to libssh by installing the ansible-pylibssh package.
+
auto will use ansible-pylibssh if that package is installed, otherwise will fallback to paramiko.
+
+
+ terminal_errors + +
+ string +
+
added in 3.1.0
+
+
    Choices: +
  • ignore
  • +
  • warn
  • +
  • fail ←
  • +
+
+
var: ansible_network_terminal_errors
+
+
This option determines how failures while setting terminal parameters are handled.
+
When set to ignore, the errors are silently ignored. When set to warn, a warning message is displayed. The default option fail, triggers a failure and halts execution.
+
+
+ terminal_inital_prompt_newline + +
+ boolean +
+
+ Default:
"yes"
+
+
var: ansible_terminal_initial_prompt_newline
+
+
This boolean flag, that when set to True will send newline in the response if any of values in terminal_initial_prompt is matched.
+
+
+ terminal_initial_answer + +
+ list + / elements=string +
+
+ +
var: ansible_terminal_initial_answer
+
+
The answer to reply with if the terminal_initial_prompt is matched. The value can be a single answer or a list of answers for multiple terminal_initial_prompt. In case the login menu has multiple prompts the sequence of the prompt and excepted answer should be in same order and the value of terminal_prompt_checkall should be set to True if all the values in terminal_initial_prompt are expected to be matched and set to False if any one login prompt is to be matched.
+
+
+ terminal_initial_prompt + +
+ list + / elements=string +
+
+ +
var: ansible_terminal_initial_prompt
+
+
A single regex pattern or a sequence of patterns to evaluate the expected prompt at the time of initial login to the remote host.
+
+
+ terminal_initial_prompt_checkall + +
+ boolean +
+
+ Default:
"no"
+
+
var: ansible_terminal_initial_prompt_checkall
+
+
By default the value is set to False and any one of the prompts mentioned in terminal_initial_prompt option is matched it won't check for other prompts. When set to True it will check for all the prompts mentioned in terminal_initial_prompt option in the given order and all the prompts should be received from remote host if not it will result in timeout.
+
+
+ terminal_stderr_re + +
+ list + / elements=dictionary +
+
+ +
var: ansible_terminal_stderr_re
+
+
This option provides the regex pattern and optional flags to match the error string from the received response chunk. This option accepts pattern and flags keys. The value of pattern is a python regex pattern to match the response and the value of flags is the value accepted by flags argument of re.compile python method to control the way regex is matched with the response, for example 're.I'.
+
+
+ terminal_stdout_re + +
+ list + / elements=dictionary +
+
+ +
var: ansible_terminal_stdout_re
+
+
A single regex pattern or a sequence of patterns along with optional flags to match the command prompt from the received response chunk. This option accepts pattern and flags keys. The value of pattern is a python regex pattern to match the response and the value of flags is the value accepted by flags argument of re.compile python method to control the way regex is matched with the response, for example 're.I'.
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.network_resource_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.network_resource_module.rst new file mode 100644 index 0000000..e99579a --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.network_resource_module.rst @@ -0,0 +1,267 @@ +.. _ansible.netcommon.network_resource_module: + + +********************************** +ansible.netcommon.network_resource +********************************** + +**Manage resource modules** + + +Version added: 2.4.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Get list of available resource modules for given os name +- Retrieve given resource module configuration facts +- Push given resource module configuration + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ raw +
+
+ +
The resource module configuration. For details on the type and structure of this option refer the individual resource module platform documentation.
+
+
+ name + +
+ string +
+
+ +
The name of the resource module to manage.
+
The resource module should be supported for given os_name, if not supported it will result in error.
+
+
+ os_name + +
+ string +
+
+ +
The name of the os to manage the resource modules.
+
The name should be fully qualified collection name format, that is <namespace>.<collection-name>.<plugin-name>.
+
If value of this option is not set the os value will be read from ansible_network_os variable.
+
If value of both os_name and ansible_network_os is not set it will result in error.
+
+
+ running_config + +
+ - +
+
+ +
This option is used only with state parsed.
+
The value of this option should be the output received from the host device by executing the cli command to get the resource configuration on host.
+
The state parsed reads the configuration from running_config option and transforms it into Ansible structured data as per the resource module's argspec and the value is then returned in the parsed key within the result.
+
+
+ state + +
+ - +
+
+ +
The state the configuration should be left in.
+
For supported values refer the individual resource module platform documentation.
+
+
+ + +Notes +----- + +.. note:: + - Refer the individual module documentation for the valid inputs of *state* and *config* modules. + + + +Examples +-------- + +.. code-block:: yaml + + - name: get list of resource modules for given network_os + ansible.netcommon.network_resource: + register: result + + - name: fetch acl config for + ansible.netcommon.network_resource: + os_name: cisco.ios.ios + name: acls + state: gathered + + - name: manage acl config for cisco.ios.ios network os. + ansible.netcommon.network_resource: + name: acls + config: + - afi: ipv4 + acls: + - name: test_acl + acl_type: extended + aces: + - grant: deny + protocol_options: + tcp: + fin: true + source: + address: 192.0.2.0 + wildcard_bits: 0.0.0.255 + destination: + address: 192.0.3.0 + wildcard_bits: 0.0.0.255 + port_protocol: + eq: www + option: + traceroute: true + ttl: + eq: 10 + state: merged + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ after + +
+ list +
+
when changed and when state and/or config option is set +
The configuration as structured data after module completion.
+
+
Sample:
+
The configuration returned will always be in the same format of the parameters above.
+
+
+ before + +
+ list +
+
When state and/or config option is set +
The configuration as structured data prior to module invocation.
+
+
Sample:
+
The configuration returned will always be in the same format of the parameters above.
+
+
+ commands + +
+ list +
+
When state and/or config option is set +
The set of commands pushed to the remote device
+
+
Sample:
+
['ip access-list extended 110']
+
+
+ modules + +
+ list +
+
When only os_name or ansible_network_os is set +
List of resource modules supported for given OS.
+
+
Sample:
+
['acl_interfaces', 'acls', 'bgp_global']
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ganesh B. Nalawade (@ganeshrn) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_cli_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_cli_filter.rst new file mode 100644 index 0000000..6c3ba8f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_cli_filter.rst @@ -0,0 +1,192 @@ +.. _ansible.netcommon.parse_cli_filter: + + +*************************** +ansible.netcommon.parse_cli +*************************** + +**parse_cli filter plugin.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- The filter plugins converts the output of a network device CLI command into structured JSON output. +- Using the parameters below - ``xml_data | ansible.netcommon.parse_cli(template.yml``) +- This plugin is deprecated and will be removed in a future release after 2027-02-01, please Use ansible.utils.cli_parse instead. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ output + +
+ raw + / required +
+
+ + +
This source data on which parse_cli invokes.
+
+
+ tmpl + +
+ string +
+
+ + +
The spec file should be valid formatted YAML. It defines how to parse the CLI output and return JSON data.
+
For example cli_data | ansible.netcommon.parse_cli(template.yml), in this case cli_data represents cli output.
+
+
+ + +Notes +----- + +.. note:: + - The parse_cli filter will load the spec file and pass the command output through it, returning JSON output. The YAML spec file defines how to parse the CLI output + + + +Examples +-------- + +.. code-block:: yaml + + # Using parse_cli + + # outputConfig + + # ip dhcp pool Data + # import all + # network 192.168.1.0 255.255.255.0 + # update dns + # default-router 192.168.1.1 + # dns-server 192.168.1.1 8.8.8.8 + # option 42 ip 192.168.1.1 + # domain-name test.local + # lease 8 + + # pconnection.yml + + # --- + # vars: + # dhcp_pool: + # name: "{{ item.name }}" + # network: "{{ item.network_ip }}" + # subnet: "{{ item.network_subnet }}" + # dns_servers: "{{ item.dns_servers_1 }}{{ item.dns_servers_2 }}" + # domain_name: "{{ item.domain_name_0 }}{{ item.domain_name_1 }}{{ item.domain_name_2 }}{{ item.domain_name_3 }}" + # options: "{{ item.options_1 }}{{ item.options_2 }}" + # lease_days: "{{ item.lease_days }}" + # lease_hours: "{{ item.lease_hours }}" + # lease_minutes: "{{ item.lease_minutes }}" + + # keys: + # dhcp_pools: + # value: "{{ dhcp_pool }}" + # items: "^ip dhcp pool ( + # ?P[^\\n]+)\\s+(?:import (?Pall)\\s*)?(?:network (?P[\\d.]+) + # (?P[\\d.]+)?\\s*)?(?:update dns\\s*)?(?:host (?P[\\d.]+) + # (?P[\\d.]+)\\s*)?(?:domain-name (?P[\\w._-]+)\\s+)? + # (?:default-router (?P[\\d.]+)\\s*)?(?:dns-server + # (?P(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name (?P[\\w._-]+)\\s+)? + # (?P(?:option [^\\n]+\\n*\\s*)*)?(?:domain-name (?P[\\w._-]+)\\s+)?(?P(?:option [^\\n]+\\n*\\s*)*)? + # (?:dns-server (?P(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name + # (?P[\\w._-]+)\\s*)?(lease (?P\\d+)(?: (?P\\d+))?(?: (?P\\d+))?\\s*)?(?:update arp)?" + + # playbook + + - name: Add config data + ansible.builtin.set_fact: + opconfig: "{{lookup('ansible.builtin.file', 'outputConfig') }}" + + - name: Parse Data + ansible.builtin.set_fact: + output: "{{ opconfig | parse_cli('pconnection.yml') }}" + + + # Task Output + # ----------- + # + # TASK [Add config data] + # ok: [host] => changed=false + # ansible_facts: + # xml: |- + # ip dhcp pool Data + # import all + # network 192.168.1.0 255.255.255.0 + # update dns + # default-router 192.168.1.1 + # dns-server 192.168.1.1 8.8.8.8 + # option 42 ip 192.168.1.1 + # domain-name test.local + # lease 8 + + # TASK [Parse Data] + # ok: [host] => changed=false + # ansible_facts: + # output: + # dhcp_pools: + # - dns_servers: 192.168.1.1 8.8.8.8 + # domain_name: test.local + # lease_days: 8 + # lease_hours: null + # lease_minutes: null + # name: Data + # network: 192.168.1.0 + # options: |- + # option 42 ip 192.168.1.1 + # subnet: 255.255.255.0 + + + + +Status +------ + + +Authors +~~~~~~~ + +- Peter Sprygada (@privateip) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_cli_textfsm_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_cli_textfsm_filter.rst new file mode 100644 index 0000000..cdbd548 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_cli_textfsm_filter.rst @@ -0,0 +1,149 @@ +.. _ansible.netcommon.parse_cli_textfsm_filter: + + +*********************************** +ansible.netcommon.parse_cli_textfsm +*********************************** + +**parse_cli_textfsm filter plugin.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- The network filters also support parsing the output of a CLI command using the TextFSM library. To parse the CLI output with TextFSM use this filter. +- Using the parameters below - ``data | ansible.netcommon.parse_cli_textfsm(template.yml``) +- This plugin is deprecated and will be removed in a future release after 2027-02-01, please Use ansible.utils.cli_parse instead. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ template + +
+ string +
+
+ + +
The template to compare it with.
+
For example data | ansible.netcommon.parse_cli_textfsm(template.yml), in this case data represents this option.
+
+
+ value + +
+ raw + / required +
+
+ + +
This source data on which parse_cli_textfsm invokes.
+
+
+ + +Notes +----- + +.. note:: + - Use of the TextFSM filter requires the TextFSM library to be installed. + + + +Examples +-------- + +.. code-block:: yaml + + # Using parse_cli_textfsm + + - name: "Fetch command output" + cisco.ios.ios_command: + commands: + - show lldp neighbors + register: lldp_output + + - name: "Invoke parse_cli_textfsm" + ansible.builtin.set_fact: + device_neighbors: "{{ lldp_output.stdout[0] | parse_cli_textfsm('~/ntc-templates/templates/cisco_ios_show_lldp_neighbors.textfsm') }}" + + - name: "Debug" + ansible.builtin.debug: + msg: "{{ device_neighbors }}" + + # Task Output + # ----------- + # + # TASK [Fetch command output] + # ok: [rtr-1] + + # TASK [Invoke parse_cli_textfsm] + # ok: [rtr-1] + + # TASK [Debug] + # ok: [rtr-1] => { + # "msg": [ + # { + # "CAPABILITIES": "R", + # "LOCAL_INTERFACE": "Gi0/0", + # "NEIGHBOR": "rtr-3", + # "NEIGHBOR_INTERFACE": "Gi0/0" + # }, + # { + # "CAPABILITIES": "R", + # "LOCAL_INTERFACE": "Gi0/1", + # "NEIGHBOR": "rtr-1", + # "NEIGHBOR_INTERFACE": "Gi0/1" + # } + # ] + # } + + + + +Status +------ + + +Authors +~~~~~~~ + +- Peter Sprygada (@privateip) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_xml_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_xml_filter.rst new file mode 100644 index 0000000..7d01cc5 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.parse_xml_filter.rst @@ -0,0 +1,224 @@ +.. _ansible.netcommon.parse_xml_filter: + + +*************************** +ansible.netcommon.parse_xml +*************************** + +**The parse_xml filter plugin.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This filter will load the spec file and pass the command output through it, returning JSON output. +- The YAML spec file defines how to parse the CLI output. +- This plugin is deprecated and will be removed in a future release after 2027-02-01, please Use ansible.utils.cli_parse instead. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ output + +
+ raw + / required +
+
+ + +
This source xml on which parse_xml invokes.
+
+
+ tmpl + +
+ string +
+
+ + +
The spec file should be valid formatted YAML. It defines how to parse the XML output and return JSON data.
+
For example xml_data | ansible.netcommon.parse_xml(template.yml), in this case xml_data represents xml data option.
+
+
+ + +Notes +----- + +.. note:: + - To convert the XML output of a network device command into structured JSON output. + + + +Examples +-------- + +.. code-block:: yaml + + # Using parse_xml + + # example_output.xml + + # + # + # + # + # + # + # 0/0/CPU0 + # + # true + # ntp-leap-no-warning + # + # + # ntp-mode-client + # true + #
10.1.1.1
+ # 0 + #
+ # -1 + #
+ # + # + # ntp-mode-client + # true + #
172.16.252.29
+ # 255 + #
+ # 991 + #
+ #
+ #
+ #
+ #
+ #
+ #
+ + # parse_xml.yml + + # --- + # vars: + # ntp_peers: + # address: "{{ item.address }}" + # reachability: "{{ item.reachability}}" + # keys: + # result: + # value: "{{ ntp_peers }}" + # top: data/ntp/nodes/node/associations + # items: + # address: peer-summary-info/peer-info-common/address + # reachability: peer-summary-info/peer-info-common/reachability + + + - name: Facts setup + ansible.builtin.set_fact: + xml: "{{ lookup('file', 'example_output.xml') }}" + + - name: Parse xml invocation + ansible.builtin.debug: + msg: "{{ xml | ansible.netcommon.parse_xml('parse_xml.yml') }}" + + + # Task Output + # ----------- + # + # TASK [set xml Data] + # ok: [host] => changed=false + # ansible_facts: + # xml: |- + # + # + # + # + # + # + # 0/0/CPU0 + # + # true + # ntp-leap-no-warning + # + # + # ntp-mode-client + # true + #
10.1.1.1
+ # 0 + #
+ # -1 + #
+ # + # + # ntp-mode-client + # true + #
172.16.252.29
+ # 255 + #
+ # 991 + #
+ #
+ #
+ #
+ #
+ #
+ #
+ + # TASK [Parse Data] + # ok: [host] => changed=false + # ansible_facts: + # output: + # result: + # - address: + # - 10.1.1.1 + # - 172.16.252.29 + # reachability: + # - '0' + # - '255' + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ganesh Nalawade (@ganeshrn) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.persistent_connection.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.persistent_connection.rst new file mode 100644 index 0000000..d2c01a4 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.persistent_connection.rst @@ -0,0 +1,151 @@ +.. _ansible.netcommon.persistent_connection: + + +**************************** +ansible.netcommon.persistent +**************************** + +**Use a persistent unix socket for connection** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This is a helper plugin to allow making other connections persistent. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ import_modules + +
+ boolean +
+
+ Default:
"yes"
+
+
ini entries: +

[ansible_network]
import_modules = yes

+
+
env:ANSIBLE_NETWORK_IMPORT_MODULES
+
var: ansible_network_import_modules
+
+
Reduce CPU usage and network module execution time by enabling direct execution. Instead of the module being packaged and executed by the shell, it will be directly executed by the Ansible control node using the same python interpreter as the Ansible process. Note- Incompatible with asynchronous mode. Note- Python 3 and Ansible 2.9.16 or greater required. Note- With Ansible 2.9.x fully qualified modules names are required in tasks.
+
+
+ persistent_command_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
command_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
+
var: ansible_command_timeout
+
+
Configures, in seconds, the amount of time to wait for a command to return from the remote device. If this timer is exceeded before the command returns, the connection plugin will raise an exception and close.
+
+
+ persistent_connect_timeout + +
+ integer +
+
+ Default:
30
+
+
ini entries: +

[persistent_connection]
connect_timeout = 30

+
+
env:ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
+
var: ansible_connect_timeout
+
+
Configures, in seconds, the amount of time to wait when trying to initially establish a persistent connection. If this value expires before the connection to the remote device is completed, the connection will fail.
+
+
+ persistent_log_messages + +
+ boolean +
+
+ Default:
"no"
+
+
ini entries: +

[persistent_connection]
log_messages = no

+
+
env:ANSIBLE_PERSISTENT_LOG_MESSAGES
+
var: ansible_persistent_log_messages
+
+
This flag will enable logging the command executed and response received from target device in the ansible log file. For this option to work 'log_path' ansible configuration option is required to be set to a file path with write access.
+
Be sure to fully understand the security implications of enabling this option as it could create a security vulnerability by logging sensitive information in log file.
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.pop_ace_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.pop_ace_filter.rst new file mode 100644 index 0000000..a8dfe84 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.pop_ace_filter.rst @@ -0,0 +1,1225 @@ +.. _ansible.netcommon.pop_ace_filter: + + +************************* +ansible.netcommon.pop_ace +************************* + +**Remove ace entries from a acl source of truth.** + + +Version added: 5.1.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This plugin removes specific keys from a provided acl data. +- Using the parameters below - ``acls_data | ansible.netcommon.pop_ace(filter_options=filter_options, match_criteria=match_criteria``) + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ data + +
+ raw + / required +
+
+ + +
This option represents a list of dictionaries of acls facts.
+
For example acls_data | ansible.netcommon.pop_ace(filter_options=filter_options, match_criteria=match_criteria), in this case acls_data represents this option.
+
+
+ filter_options + +
+ dictionary +
+
+ + +
Specify filtering options which drives the filter plugin.
+
+
+ failed_when + +
+ string +
+
+
    Choices: +
  • missing ←
  • +
  • never
  • +
+
+ +
On missing it fails when there is no match with the ACL data supplied
+
On never it would never fail
+
+
+ match_all + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+ +
When true ensures ace removed only when it matches all match criteria
+
+
+ remove + +
+ string +
+
+
    Choices: +
  • first
  • +
  • all ←
  • +
+
+ +
Remove first removes one ace from each ACL entry on match
+
Remove all is more aggressive and removes more than one on match
+
+
+ match_criteria + +
+ dictionary + / required +
+
+ + +
Specify the matching configuration of the ACEs to remove.
+
+
+ acl_name + +
+ string +
+
+ + +
ACL name to match
+
+
+ afi + +
+ string + / required +
+
+ + +
Specify afi to match
+
+
+ destination + +
+ string +
+
+ + +
Destination address/ host/ any of the ACE to natch
+
+
+ grant + +
+ string +
+
+ + +
Grant type permit or deny to match
+
+
+ protocol + +
+ string +
+
+ + +
Protocol name of the ACE to match
+
+
+ sequence + +
+ string +
+
+ + +
Sequence number of the ACE to match
+
+
+ source + +
+ string +
+
+ + +
Source address/ host/ any of the ACE to match
+
+
+ + +Notes +----- + +.. note:: + - The filter plugin has been tested with facts collected for acls resource module on Cisco IOSXE, IOSXR and NXOS. + + + +Examples +-------- + +.. code-block:: yaml + + ## Playbook with filter plugin example + vars: + filter_options: + match_all: true + match_criteria: + afi: "ipv4" + source: "192.0.2.0" + destination: "192.0.3.0" + acls_data: + - acls: + - aces: + - destination: + address: 192.0.3.0 + wildcard_bits: 0.0.0.255 + dscp: ef + grant: deny + protocol: icmp + protocol_options: + icmp: + traceroute: true + sequence: 10 + source: + address: 192.0.2.0 + wildcard_bits: 0.0.0.255 + ttl: + eq: 10 + - destination: + host: 198.51.110.0 + port_protocol: + eq: telnet + grant: deny + protocol: tcp + protocol_options: + tcp: + ack: true + sequence: 20 + source: + host: 198.51.100.0 + acl_type: extended + name: "110" + - aces: + - destination: + address: 198.51.101.0 + port_protocol: + eq: telnet + wildcard_bits: 0.0.0.255 + grant: deny + protocol: tcp + protocol_options: + tcp: + ack: true + sequence: 10 + source: + address: 198.51.100.0 + wildcard_bits: 0.0.0.255 + tos: + service_value: 12 + - destination: + address: 192.0.4.0 + port_protocol: + eq: www + wildcard_bits: 0.0.0.255 + dscp: ef + grant: deny + protocol: tcp + protocol_options: + tcp: + ack: true + sequence: 20 + source: + address: 192.0.3.0 + wildcard_bits: 0.0.0.255 + ttl: + lt: 20 + acl_type: extended + name: "123" + - aces: + - grant: deny + sequence: 10 + source: + host: 192.168.1.200 + - grant: deny + sequence: 20 + source: + address: 192.168.2.0 + wildcard_bits: 0.0.0.255 + acl_type: standard + name: std_acl + - aces: + - destination: + address: 192.0.3.0 + port_protocol: + eq: www + wildcard_bits: 0.0.0.255 + grant: deny + option: + traceroute: true + protocol: tcp + protocol_options: + tcp: + fin: true + sequence: 10 + source: + address: 192.0.2.0 + wildcard_bits: 0.0.0.255 + ttl: + eq: 10 + acl_type: extended + name: test + afi: ipv4 + - acls: + - aces: + - destination: + any: true + port_protocol: + eq: telnet + dscp: af11 + grant: deny + protocol: tcp + protocol_options: + tcp: + ack: true + sequence: 10 + source: + any: true + port_protocol: + eq: www + name: R1_TRAFFIC + afi: ipv6 + + tasks: + - name: Remove ace entries from a provided data + ansible.builtin.debug: + msg: "{{ acls_data | ansible.netcommon.pop_ace(filter_options=filter_options, match_criteria=match_criteria) }}" + + ## Output + # PLAY [Filter plugin example pop_ace] ****************************************************************************************************************** + + # TASK [Remove ace entries from a provided data] *********************************************************************************************************** + # ok: [xe_machine] => + # msg: + # clean_acls: + # acls: + # - acls: + # - aces: + # - destination: + # host: 198.51.110.0 + # port_protocol: + # eq: telnet + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # host: 198.51.100.0 + # name: '110' + # - aces: + # - destination: + # address: 198.51.101.0 + # port_protocol: + # eq: telnet + # wildcard_bits: 0.0.0.255 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # address: 198.51.100.0 + # wildcard_bits: 0.0.0.255 + # tos: + # service_value: 12 + # - destination: + # address: 192.0.4.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # lt: 20 + # name: '123' + # - aces: + # - grant: deny + # sequence: 10 + # source: + # host: 192.168.1.200 + # - grant: deny + # sequence: 20 + # source: + # address: 192.168.2.0 + # wildcard_bits: 0.0.0.255 + # name: std_acl + # afi: ipv4 + # - acls: + # - aces: + # - destination: + # any: true + # port_protocol: + # eq: telnet + # dscp: af11 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # any: true + # port_protocol: + # eq: www + # name: R1_TRAFFIC + # afi: ipv6 + # removed_aces: + # acls: + # - acls: + # - aces: + # - destination: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: icmp + # protocol_options: + # icmp: + # traceroute: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # name: '110' + # - aces: + # - destination: + # address: 192.0.3.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # grant: deny + # option: + # traceroute: true + # protocol: tcp + # protocol_options: + # tcp: + # fin: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # name: test + # afi: ipv4 + # - acls: [] + # afi: ipv6 + + + ## Playbook with workflow example + _tasks: + - name: Gather ACLs config from device existing ACLs config + cisco.ios.ios_acls: + state: gathered + register: result_gathered + + - name: Setting host facts for pop_ace filter plugin + ansible.builtin.set_fact: + acls_facts: "{{ result_gathered.gathered }}" + filter_options: + match_all: true + match_criteria: + afi: "ipv4" + source: "192.0.2.0" + destination: "192.0.3.0" + + - name: Invoke pop_ace filter plugin + ansible.builtin.set_fact: + clean_acls: "{{ acls_facts | ansible.netcommon.pop_ace(filter_options=filter_options, match_criteria=match_criteria) }}" + + - name: Override ACLs config with device existing ACLs config + cisco.ios.ios_acls: + state: overridden + config: "{{ clean_acls['clean_acls']['acls'] | from_yaml }}" + + ## Output + + # PLAYBOOK: pop_ace_example.yml *********************************************** + + # PLAY [Filter plugin example pop_ace] **************************************** + + # TASK [Gather ACLs config with device existing ACLs config] ********************* + # ok: [xe_machine] => changed=false + # gathered: + # - acls: + # - aces: + # - destination: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: icmp + # protocol_options: + # icmp: + # traceroute: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # - destination: + # host: 198.51.110.0 + # port_protocol: + # eq: telnet + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # host: 198.51.100.0 + # acl_type: extended + # name: '110' + # - aces: + # - destination: + # address: 198.51.101.0 + # port_protocol: + # eq: telnet + # wildcard_bits: 0.0.0.255 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # address: 198.51.100.0 + # wildcard_bits: 0.0.0.255 + # tos: + # service_value: 12 + # - destination: + # address: 192.0.4.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # lt: 20 + # acl_type: extended + # name: '123' + # - aces: + # - grant: deny + # sequence: 10 + # source: + # host: 192.168.1.200 + # - grant: deny + # sequence: 20 + # source: + # address: 192.168.2.0 + # wildcard_bits: 0.0.0.255 + # acl_type: standard + # name: std_acl + # - aces: + # - destination: + # address: 192.0.3.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # grant: deny + # option: + # traceroute: true + # protocol: tcp + # protocol_options: + # tcp: + # fin: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # acl_type: extended + # name: test + # afi: ipv4 + # - acls: + # - aces: + # - destination: + # any: true + # port_protocol: + # eq: telnet + # dscp: af11 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # any: true + # port_protocol: + # eq: www + # name: R1_TRAFFIC + # afi: ipv6 + # invocation: + # module_args: + # config: null + # running_config: null + # state: gathered + + # TASK [Setting host facts for pop_ace filter plugin] ************************* + # ok: [xe_machine] => changed=false + # ansible_facts: + # acls_facts: + # - acls: + # - aces: + # - destination: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: icmp + # protocol_options: + # icmp: + # traceroute: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # - destination: + # host: 198.51.110.0 + # port_protocol: + # eq: telnet + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # host: 198.51.100.0 + # acl_type: extended + # name: '110' + # - aces: + # - destination: + # address: 198.51.101.0 + # port_protocol: + # eq: telnet + # wildcard_bits: 0.0.0.255 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # address: 198.51.100.0 + # wildcard_bits: 0.0.0.255 + # tos: + # service_value: 12 + # - destination: + # address: 192.0.4.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # lt: 20 + # acl_type: extended + # name: '123' + # - aces: + # - grant: deny + # sequence: 10 + # source: + # host: 192.168.1.200 + # - grant: deny + # sequence: 20 + # source: + # address: 192.168.2.0 + # wildcard_bits: 0.0.0.255 + # acl_type: standard + # name: std_acl + # - aces: + # - destination: + # address: 192.0.3.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # grant: deny + # option: + # traceroute: true + # protocol: tcp + # protocol_options: + # tcp: + # fin: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # acl_type: extended + # name: test + # afi: ipv4 + # - acls: + # - aces: + # - destination: + # any: true + # port_protocol: + # eq: telnet + # dscp: af11 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # any: true + # port_protocol: + # eq: www + # name: R1_TRAFFIC + # afi: ipv6 + # filter_options: + # match_all: true + # match_criteria: + # afi: ipv4 + # destination: 192.0.3.0 + # source: 192.0.2.0 + + # TASK [Invoke pop_ace filter plugin] ***************************************** + # ok: [xe_machine] => changed=false + # ansible_facts: + # clean_acls: + # clean_acls: + # acls: + # - acls: + # - aces: + # - destination: + # host: 198.51.110.0 + # port_protocol: + # eq: telnet + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # host: 198.51.100.0 + # name: '110' + # - aces: + # - destination: + # address: 198.51.101.0 + # port_protocol: + # eq: telnet + # wildcard_bits: 0.0.0.255 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # address: 198.51.100.0 + # wildcard_bits: 0.0.0.255 + # tos: + # service_value: 12 + # - destination: + # address: 192.0.4.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # lt: 20 + # name: '123' + # - aces: + # - grant: deny + # sequence: 10 + # source: + # host: 192.168.1.200 + # - grant: deny + # sequence: 20 + # source: + # address: 192.168.2.0 + # wildcard_bits: 0.0.0.255 + # name: std_acl + # afi: ipv4 + # - acls: + # - aces: + # - destination: + # any: true + # port_protocol: + # eq: telnet + # dscp: af11 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # any: true + # port_protocol: + # eq: www + # name: R1_TRAFFIC + # afi: ipv6 + # removed_aces: + # acls: + # - acls: + # - aces: + # - destination: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: icmp + # protocol_options: + # icmp: + # traceroute: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # name: '110' + # - aces: + # - destination: + # address: 192.0.3.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # grant: deny + # option: + # traceroute: true + # protocol: tcp + # protocol_options: + # tcp: + # fin: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # name: test + # afi: ipv4 + # - acls: [] + # afi: ipv6 + + # TASK [Override ACLs config with device existing ACLs config] ******************* + # changed: [xe_machine] => changed=true + # after: + # - acls: + # - aces: + # - destination: + # host: 198.51.110.0 + # port_protocol: + # eq: telnet + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # host: 198.51.100.0 + # acl_type: extended + # name: '110' + # - aces: + # - destination: + # address: 198.51.101.0 + # port_protocol: + # eq: telnet + # wildcard_bits: 0.0.0.255 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # address: 198.51.100.0 + # wildcard_bits: 0.0.0.255 + # tos: + # service_value: 12 + # - destination: + # address: 192.0.4.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # lt: 20 + # acl_type: extended + # name: '123' + # - aces: + # - grant: deny + # sequence: 10 + # source: + # host: 192.168.1.200 + # - grant: deny + # sequence: 20 + # source: + # address: 192.168.2.0 + # wildcard_bits: 0.0.0.255 + # acl_type: standard + # name: std_acl + # afi: ipv4 + # - acls: + # - aces: + # - destination: + # any: true + # port_protocol: + # eq: telnet + # dscp: af11 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # any: true + # port_protocol: + # eq: www + # name: R1_TRAFFIC + # afi: ipv6 + # before: + # - acls: + # - aces: + # - destination: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: icmp + # protocol_options: + # icmp: + # traceroute: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # - destination: + # host: 198.51.110.0 + # port_protocol: + # eq: telnet + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # host: 198.51.100.0 + # acl_type: extended + # name: '110' + # - aces: + # - destination: + # address: 198.51.101.0 + # port_protocol: + # eq: telnet + # wildcard_bits: 0.0.0.255 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # address: 198.51.100.0 + # wildcard_bits: 0.0.0.255 + # tos: + # service_value: 12 + # - destination: + # address: 192.0.4.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # dscp: ef + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 20 + # source: + # address: 192.0.3.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # lt: 20 + # acl_type: extended + # name: '123' + # - aces: + # - grant: deny + # sequence: 10 + # source: + # host: 192.168.1.200 + # - grant: deny + # sequence: 20 + # source: + # address: 192.168.2.0 + # wildcard_bits: 0.0.0.255 + # acl_type: standard + # name: std_acl + # - aces: + # - destination: + # address: 192.0.3.0 + # port_protocol: + # eq: www + # wildcard_bits: 0.0.0.255 + # grant: deny + # option: + # traceroute: true + # protocol: tcp + # protocol_options: + # tcp: + # fin: true + # sequence: 10 + # source: + # address: 192.0.2.0 + # wildcard_bits: 0.0.0.255 + # ttl: + # eq: 10 + # acl_type: extended + # name: test + # afi: ipv4 + # - acls: + # - aces: + # - destination: + # any: true + # port_protocol: + # eq: telnet + # dscp: af11 + # grant: deny + # protocol: tcp + # protocol_options: + # tcp: + # ack: true + # sequence: 10 + # source: + # any: true + # port_protocol: + # eq: www + # name: R1_TRAFFIC + # afi: ipv6 + # commands: + # - ip access-list extended 110 + # - no 10 deny icmp 192.0.2.0 0.0.0.255 192.0.3.0 0.0.0.255 traceroute dscp ef ttl eq 10 + # - no ip access-list extended test + + + + +Status +------ + + +Authors +~~~~~~~ + +- Sagar Paul (@KB-perByte) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_config_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_config_module.rst new file mode 100644 index 0000000..5db4bb4 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_config_module.rst @@ -0,0 +1,226 @@ +.. _ansible.netcommon.restconf_config_module: + + +********************************* +ansible.netcommon.restconf_config +********************************* + +**Handles create, update, read and delete of configuration data on RESTCONF enabled devices.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- RESTCONF is a standard mechanisms to allow web applications to configure and manage data. RESTCONF is a IETF standard and documented on RFC 8040. +- This module allows the user to configure data on RESTCONF enabled devices. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ content + +
+ string +
+
+ +
The configuration data in format as specififed in format option. Required unless method is delete.
+
+
+ format + +
+ string +
+
+
    Choices: +
  • json ←
  • +
  • xml
  • +
+
+
The format of the configuration provided as value of content. Accepted values are xml and json and the given configuration format should be supported by remote RESTCONF server.
+
+
+ method + +
+ string +
+
+
    Choices: +
  • post ←
  • +
  • put
  • +
  • patch
  • +
  • delete
  • +
+
+
The RESTCONF method to manage the configuration change on device. The value post is used to create a data resource or invoke an operation resource, put is used to replace the target data resource, patch is used to modify the target resource, and delete is used to delete the target resource.
+
+
+ path + +
+ string + / required +
+
+ +
URI being used to execute API calls.
+
+
+ + +Notes +----- + +.. note:: + - This module requires the RESTCONF system service be enabled on the remote device being managed. + - This module is supported with *ansible_connection* value of *ansible.netcommon.httpapi* and *ansible_network_os* value of *ansible.netcommon.restconf*. + - This module is tested against Cisco IOSXE 16.12.02 version. + + + +Examples +-------- + +.. code-block:: yaml + + - name: create l3vpn services + ansible.netcommon.restconf_config: + path: /config/ietf-l3vpn-svc:l3vpn-svc/vpn-services + content: | + { + "vpn-service":[ + { + "vpn-id": "red_vpn2", + "customer-name": "blue", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + }, + { + "vpn-id": "blue_vpn1", + "customer-name": "red", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + } + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ candidate + +
+ dictionary +
+
When the method is not delete +
The configuration sent to the device.
+
+
Sample:
+
{ + "vpn-service": [ + { + "customer-name": "red", + "vpn-id": "blue_vpn1", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + }
+
+
+ running + +
+ dictionary +
+
When the method is not delete +
The current running configuration on the device.
+
+
Sample:
+
{ + "vpn-service": [ + { + "vpn-id": "red_vpn2", + "customer-name": "blue", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + }, + { + "vpn-id": "blue_vpn1", + "customer-name": "red", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + }
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ganesh Nalawade (@ganeshrn) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_get_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_get_module.rst new file mode 100644 index 0000000..e271438 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_get_module.rst @@ -0,0 +1,167 @@ +.. _ansible.netcommon.restconf_get_module: + + +****************************** +ansible.netcommon.restconf_get +****************************** + +**Fetch configuration/state data from RESTCONF enabled devices.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- RESTCONF is a standard mechanisms to allow web applications to access the configuration data and state data developed and standardized by the IETF. It is documented in RFC 8040. +- This module allows the user to fetch configuration and state data from RESTCONF enabled devices. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ content + +
+ string +
+
+
    Choices: +
  • config
  • +
  • nonconfig
  • +
  • all
  • +
+
+
The content is a query parameter that controls how descendant nodes of the requested data nodes in path will be processed in the reply. If value is config return only configuration descendant data nodes of value in path. If value is nonconfig return only non-configuration descendant data nodes of value in path. If value is all return all descendant data nodes of value in path
+
+
+ output + +
+ string +
+
+
    Choices: +
  • json ←
  • +
  • xml
  • +
+
+
The output of response received.
+
+
+ path + +
+ string + / required +
+
+ +
URI being used to execute API calls.
+
+
+ + +Notes +----- + +.. note:: + - This module requires the RESTCONF system service be enabled on the remote device being managed. + - This module is supported with *ansible_connection* value of *ansible.netcommon.httpapi* and *ansible_network_os* value of *ansible.netcommon.restconf*. + - This module is tested against Cisco IOSXE 16.12.02 version. + + + +Examples +-------- + +.. code-block:: yaml + + - name: get l3vpn services + ansible.netcommon.restconf_get: + path: /config/ietf-l3vpn-svc:l3vpn-svc/vpn-services + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + +
KeyReturnedDescription
+
+ response + +
+ dictionary +
+
when the device response is valid JSON +
A dictionary representing a JSON-formatted response
+
+
Sample:
+
{ + "vpn-services": { + "vpn-service": [ + { + "customer-name": "red", + "vpn-id": "blue_vpn1", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + } + }
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ganesh Nalawade (@ganeshrn) diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_httpapi.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_httpapi.rst new file mode 100644 index 0000000..00031ed --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.restconf_httpapi.rst @@ -0,0 +1,77 @@ +.. _ansible.netcommon.restconf_httpapi: + + +************************** +ansible.netcommon.restconf +************************** + +**HttpApi Plugin for devices supporting Restconf API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This HttpApi plugin provides methods to connect to Restconf API endpoints. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ root_path + +
+ string +
+
+ Default:
"/restconf"
+
+
var: ansible_httpapi_restconf_root
+
+
Specifies the location of the Restconf root.
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Networking Team (@ansible-network) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.telnet_module.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.telnet_module.rst new file mode 100644 index 0000000..18a9769 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.telnet_module.rst @@ -0,0 +1,319 @@ +.. _ansible.netcommon.telnet_module: + + +************************ +ansible.netcommon.telnet +************************ + +**Executes a low-down and dirty telnet command** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Executes a low-down and dirty telnet command, not going through the module subsystem. +- This is mostly to be used for enabling ssh on devices that only have telnet enabled by default. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ command + +
+ list + / elements=string + / required +
+
+ +
List of commands to be executed in the telnet session.
+

aliases: commands
+
+
+ crlf + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Sends a CRLF (Carrage Return) instead of just a LF (Line Feed).
+
+
+ host + +
+ string +
+
+ Default:
"remote_addr"
+
+
The host/target on which to execute the command
+
+
+ login_prompt + +
+ string +
+
+ Default:
"login: "
+
+
Login or username prompt to expect
+
+
+ password + +
+ string +
+
+ +
The password for login
+
+
+ password_prompt + +
+ string +
+
+ Default:
"Password: "
+
+
Login or username prompt to expect
+
+
+ pause + +
+ integer +
+
+ Default:
1
+
+
Seconds to pause between each command issued
+
+
+ port + +
+ integer +
+
+ Default:
23
+
+
Remote port to use
+
+
+ prompts + +
+ list + / elements=string +
+
+ Default:
["$"]
+
+
List of prompts expected before sending next command
+
+
+ send_newline + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Sends a newline character upon successful connection to start the terminal session.
+
+
+ timeout + +
+ integer +
+
+ Default:
120
+
+
timeout for remote operations
+
+
+ user + +
+ string +
+
+ Default:
"remote_user"
+
+
The user for login
+
+
+ + +Notes +----- + +.. note:: + - The ``environment`` keyword does not work with this task + + + +Examples +-------- + +.. code-block:: yaml + + - name: send configuration commands to IOS + ansible.netcommon.telnet: + user: cisco + password: cisco + login_prompt: "Username: " + prompts: + - "[>#]" + command: + - terminal length 0 + - configure terminal + - hostname ios01 + + - name: run show commands + ansible.netcommon.telnet: + user: cisco + password: cisco + login_prompt: "Username: " + prompts: + - "[>#]" + command: + - terminal length 0 + - show version + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + +
KeyReturnedDescription
+
+ output + +
+ list +
+
always +
output of each command is an element in this list
+
+
Sample:
+
['success', 'success', '', 'warning .. something']
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Ansible Core Team diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.type5_pw_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.type5_pw_filter.rst new file mode 100644 index 0000000..0d4e6a0 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.type5_pw_filter.rst @@ -0,0 +1,128 @@ +.. _ansible.netcommon.type5_pw_filter: + + +************************** +ansible.netcommon.type5_pw +************************** + +**The type5_pw filter plugin.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Filter plugin to produce cisco type5 hashed password. +- Using the parameters below - ``xml_data | ansible.netcommon.type5_pw(template.yml``) +- This plugin uses do_encrypt if used with ansible-core 2.20+ and passlib_or_crypt for versions before 2.20 + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ password + +
+ string + / required +
+
+ + +
The password to be hashed.
+
+
+ salt + +
+ string +
+
+ + +
Mention the salt to hash the password.
+
+
+ + +Notes +----- + +.. note:: + - The filter plugin generates cisco type5 hashed password. + + + +Examples +-------- + +.. code-block:: yaml + + # Using type5_pw + + - name: Set some facts + ansible.builtin.set_fact: + password: "cisco@123" + + - name: Filter type5_pw invocation + ansible.builtin.debug: + msg: "{{ password | ansible.netcommon.type5_pw(salt='avs') }}" + + + # Task Output + # ----------- + # + # TASK [Set some facts] + # ok: [host] => changed=false + # ansible_facts: + # password: cisco@123 + + # TASK [Filter type5_pw invocation] + # ok: [host] => + # msg: $1$avs$uSTOEMh65qzvpb9yBMpzd/ + + + + +Status +------ + + +Authors +~~~~~~~ + +- Ken Celenza (@itdependsnetworks) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.vlan_expander_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.vlan_expander_filter.rst new file mode 100644 index 0000000..bcde65b --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.vlan_expander_filter.rst @@ -0,0 +1,119 @@ +.. _ansible.netcommon.vlan_expander_filter: + + +******************************* +ansible.netcommon.vlan_expander +******************************* + +**The vlan_expander filter plugin.** + + +Version added: 2.3.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Expand shorthand list of VLANs to list all VLANs. Inverse of vlan_parser +- Using the parameters below - ``vlans_data | ansible.netcommon.vlan_expander`` + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ data + +
+ string + / required +
+
+ + +
This option represents a string containing the range of vlans.
+
+
+ + +Notes +----- + +.. note:: + - The filter plugin extends vlans when data provided in range or comma separated. + + + +Examples +-------- + +.. code-block:: yaml + + # Using vlan_expander + + - name: Setting host facts for vlan_expander filter plugin + ansible.builtin.set_fact: + vlan_ranges: "1,10-12,15,20-22" + + - name: Invoke vlan_expander filter plugin + ansible.builtin.set_fact: + extended_vlans: "{{ vlan_ranges | ansible.netcommon.vlan_expander }}" + + + # Task Output + # ----------- + # + # TASK [Setting host facts for vlan_expander filter plugin] + # ok: [host] => changed=false + # ansible_facts: + # vlan_ranges: 1,10-12,15,20-22 + + # TASK [Invoke vlan_expander filter plugin] + # ok: [host] => changed=false + # ansible_facts: + # extended_vlans: + # - 1 + # - 10 + # - 11 + # - 12 + # - 15 + # - 20 + # - 21 + # - 22 + + + + +Status +------ + + +Authors +~~~~~~~ + +- Akira Yokochi (@akira6592) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.vlan_parser_filter.rst b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.vlan_parser_filter.rst new file mode 100644 index 0000000..43419d0 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/docs/ansible.netcommon.vlan_parser_filter.rst @@ -0,0 +1,181 @@ +.. _ansible.netcommon.vlan_parser_filter: + + +***************************** +ansible.netcommon.vlan_parser +***************************** + +**The vlan_parser filter plugin.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- The filter plugin converts a list of vlans to IOS like vlan configuration. +- Converts list to a list of range of numbers into multiple lists. +- ``vlans_data | ansible.netcommon.vlan_parser(first_line_len = 20, other_line_len=20``) + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ data + +
+ list + / required +
+
+ + +
This option represents a list containing vlans.
+
+
+ first_line_len + +
+ integer +
+
+ Default:
48
+
+ +
The first line of the list can be first_line_len characters long.
+
+
+ other_line_len + +
+ integer +
+
+ Default:
44
+
+ +
The subsequent list lines can be other_line_len characters.
+
+
+ + +Notes +----- + +.. note:: + - The filter plugin extends vlans when data provided in range or comma separated. + + + +Examples +-------- + +.. code-block:: yaml + + # Using vlan_parser + + - name: Setting host facts for vlan_parser filter plugin + ansible.builtin.set_fact: + vlans: + [ + 100, + 1688, + 3002, + 3003, + 3004, + 3005, + 3102, + 3103, + 3104, + 3105, + 3802, + 3900, + 3998, + 3999, + ] + + - name: Invoke vlan_parser filter plugin + ansible.builtin.set_fact: + vlans_ranges: "{{ vlans | ansible.netcommon.vlan_parser(first_line_len = 20, other_line_len=20) }}" + + + # Task Output + # ----------- + # + # TASK [Setting host facts for vlan_parser filter plugin] + # ok: [host] => changed=false + # ansible_facts: + # vlans: + # - 100 + # - 1688 + # - 3002 + # - 3003 + # - 3004 + # - 3005 + # - 3102 + # - 3103 + # - 3104 + # - 3105 + # - 3802 + # - 3900 + # - 3998 + # - 3999 + + # TASK [Invoke vlan_parser filter plugin] + # ok: [host] => changed=false + # ansible_facts: + # msg: + # - 100,1688,3002-3005 + # - 3102-3105,3802,3900 + # - 3998,3999 + + + + +Status +------ + + +Authors +~~~~~~~ + +- Steve Dodd (@idahood) + + +.. hint:: + Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up. diff --git a/collections/ansible_collections/ansible/netcommon/meta/runtime.yml b/collections/ansible_collections/ansible/netcommon/meta/runtime.yml new file mode 100644 index 0000000..7044a60 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/meta/runtime.yml @@ -0,0 +1,15 @@ +--- +requires_ansible: ">=2.16.0" +plugin_routing: + action: + grpc_config: + redirect: ansible.netcommon.grpc + grpc_get: + redirect: ansible.netcommon.grpc + filter: + ipaddr: + redirect: ansible.utils.ipaddr + ipv4: + redirect: ansible.utils.ipv4 + ipv6: + redirect: ansible.utils.ipv6 diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/cli_backup.py b/collections/ansible_collections/ansible/netcommon/plugins/action/cli_backup.py new file mode 100644 index 0000000..9cfd413 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/cli_backup.py @@ -0,0 +1,31 @@ +# +# Copyright 2018 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 + +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + if self._play_context.connection.split(".")[-1] != "network_cli": + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + result = super(ActionModule, self).run(task_vars=task_vars) + self._handle_backup_option( + result, + task_vars, + self._task.args, + ) + + return result diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/cli_command.py b/collections/ansible_collections/ansible/netcommon/plugins/action/cli_command.py new file mode 100644 index 0000000..15742fd --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/cli_command.py @@ -0,0 +1,24 @@ +# +# Copyright 2018 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 + +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + if self._play_context.connection.split(".")[-1] != "network_cli": + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + return super(ActionModule, self).run(task_vars=task_vars) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/cli_config.py b/collections/ansible_collections/ansible/netcommon/plugins/action/cli_config.py new file mode 100644 index 0000000..ca4aa53 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/cli_config.py @@ -0,0 +1,28 @@ +# +# Copyright 2018 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 + +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + self._config_module = True + if self._play_context.connection.split(".")[-1] != "network_cli": + return { + "failed": True, + "msg": "Connection type %s is not valid for cli_config module" + % self._play_context.connection, + } + + return super(ActionModule, self).run(task_vars=task_vars) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/grpc.py b/collections/ansible_collections/ansible/netcommon/plugins/action/grpc.py new file mode 100644 index 0000000..34be699 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/grpc.py @@ -0,0 +1,35 @@ +# +# Copyright 2022 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 + +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + self._config_module = True + warnings = [] + if self._play_context.connection.split(".")[-1] != "grpc": + return { + "failed": True, + "msg": "Connection type %s is not valid for grpc module. " + "Valid connection type is grpc" % self._play_context.connection, + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py b/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py new file mode 100644 index 0000000..56393f6 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/net_get.py @@ -0,0 +1,182 @@ +# (c) 2018, Ansible 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 hashlib +import os +import re +import uuid + +from ansible.errors import AnsibleError +from ansible.module_utils.common.text.converters import to_bytes, to_text +from ansible.module_utils.connection import Connection, ConnectionError +from ansible.module_utils.six.moves.urllib.parse import urlsplit +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display + + +display = Display() + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + changed = False + socket_path = None + self._get_network_os(task_vars) + persistent_connection = self._play_context.connection.split(".")[-1] + + result = super(ActionModule, self).run(task_vars=task_vars) + + if persistent_connection != "network_cli": + # It is supported only with network_cli + result["failed"] = True + result["msg"] = ( + "connection type %s is not valid for net_get module," + " please use fully qualified name of network_cli connection type" + % self._play_context.connection + ) + return result + + try: + src = self._task.args["src"] + except KeyError as exc: + return { + "failed": True, + "msg": "missing required argument: %s" % exc, + } + + # Get destination file if specified + dest = self._task.args.get("dest") + + if dest is None: + dest = self._get_default_dest(src) + else: + dest = self._handle_dest_path(dest) + + # Get proto + proto = self._task.args.get("protocol") + if proto is None: + proto = "scp" + + if socket_path is None: + socket_path = self._connection.socket_path + + conn = Connection(socket_path) + sock_timeout = conn.get_option("persistent_command_timeout") + + try: + changed = self._handle_existing_file(conn, src, dest, proto, sock_timeout) + if changed is False: + result["changed"] = changed + result["destination"] = dest + return result + except Exception as exc: + result["msg"] = "Warning: %s idempotency check failed. Check dest" % exc + + try: + conn.get_file(source=src, destination=dest, proto=proto, timeout=sock_timeout) + except Exception as exc: + result["failed"] = True + result["msg"] = "Exception received: %s" % exc + + result["changed"] = changed + result["destination"] = dest + return result + + def _handle_dest_path(self, dest): + working_path = self._get_working_path() + + if os.path.isabs(dest) or urlsplit("dest").scheme: + dst = dest + else: + dst = self._loader.path_dwim_relative(working_path, "", dest) + + return dst + + def _get_src_filename_from_path(self, src_path): + filename_list = re.split("/|:", src_path) + return filename_list[-1] + + def _get_default_dest(self, src_path): + dest_path = self._get_working_path() + src_fname = self._get_src_filename_from_path(src_path) + filename = "%s/%s" % (dest_path, src_fname) + return filename + + def _handle_existing_file(self, conn, source, dest, proto, timeout): + """ + Determines whether the source and destination file match. + + :return: False if source and dest both exist and have matching sha1 sums, True otherwise. + """ + if not os.path.exists(dest): + return True + + cwd = self._loader.get_basedir() + filename = str(uuid.uuid4()) + tmp_dest_file = os.path.join(cwd, filename) + try: + conn.get_file( + source=source, + destination=tmp_dest_file, + proto=proto, + timeout=timeout, + ) + except ConnectionError as exc: + error = to_text(exc) + if error.endswith("No such file or directory"): + if os.path.exists(tmp_dest_file): + os.remove(tmp_dest_file) + return True + + try: + with open(tmp_dest_file, "r") as f: + new_content = f.read() + with open(dest, "r") as f: + old_content = f.read() + except (IOError, OSError): + os.remove(tmp_dest_file) + raise + + sha1 = hashlib.sha1() + old_content_b = to_bytes(old_content, errors="surrogate_or_strict") + sha1.update(old_content_b) + checksum_old = sha1.digest() + + sha1 = hashlib.sha1() + new_content_b = to_bytes(new_content, errors="surrogate_or_strict") + sha1.update(new_content_b) + checksum_new = sha1.digest() + os.remove(tmp_dest_file) + if checksum_old == checksum_new: + return False + return True + + def _get_working_path(self): + cwd = self._loader.get_basedir() + if self._task._role is not None: + cwd = self._task._role._role_path + return cwd + + def _get_network_os(self, task_vars): + if "network_os" in self._task.args and self._task.args["network_os"]: + display.vvvv("Getting network OS from task argument") + network_os = self._task.args["network_os"] + elif self._play_context.network_os: + display.vvvv("Getting network OS from inventory") + network_os = self._play_context.network_os + elif ( + "network_os" in task_vars.get("ansible_facts", {}) + and task_vars["ansible_facts"]["network_os"] + ): + display.vvvv("Getting network OS from fact") + network_os = task_vars["ansible_facts"]["network_os"] + else: + raise AnsibleError("ansible_network_os must be specified on this host") + + return network_os diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/net_ping.py b/collections/ansible_collections/ansible/netcommon/plugins/action/net_ping.py new file mode 100644 index 0000000..109e613 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/net_ping.py @@ -0,0 +1,87 @@ +# (c) 2017, Ansible by 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 copy + +from ansible.errors import AnsibleError +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display + + +display = Display() + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + del tmp # tmp no longer has any effect + + result = {} + play_context = copy.deepcopy(self._play_context) + play_context.network_os = self._get_network_os(task_vars) + new_task = self._task.copy() + + module = self._get_implementation_module(play_context.network_os, self._task.action) + if not module: + if self._task.args["fail_on_missing_module"]: + result["failed"] = True + else: + result["failed"] = False + + result["msg"] = "Could not find implementation module %s for %s" % ( + self._task.action, + play_context.network_os, + ) + return result + + new_task.action = module + + action = self._shared_loader_obj.action_loader.get( + play_context.network_os, + task=new_task, + connection=self._connection, + play_context=play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj, + ) + display.vvvv("Running implementation module %s" % module) + return action.run(task_vars=task_vars) + + def _get_network_os(self, task_vars): + if "network_os" in self._task.args and self._task.args["network_os"]: + display.vvvv("Getting network OS from task argument") + network_os = self._task.args["network_os"] + elif self._play_context.network_os: + display.vvvv("Getting network OS from inventory") + network_os = self._play_context.network_os + elif ( + "network_os" in task_vars.get("ansible_facts", {}) + and task_vars["ansible_facts"]["network_os"] + ): + display.vvvv("Getting network OS from fact") + network_os = task_vars["ansible_facts"]["network_os"] + else: + raise AnsibleError( + "ansible_network_os must be specified on this host to use platform agnostic modules" + ) + + return network_os + + def _get_implementation_module(self, network_os, platform_agnostic_module): + module_name = network_os.split(".")[-1] + "_" + platform_agnostic_module.partition("_")[2] + if "." in network_os: + fqcn_module = ".".join(network_os.split(".")[0:-1]) + implementation_module = fqcn_module + "." + module_name + else: + implementation_module = module_name + + if implementation_module not in self._shared_loader_obj.module_loader: + implementation_module = None + + return implementation_module diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py b/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py new file mode 100644 index 0000000..d67f470 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/net_put.py @@ -0,0 +1,256 @@ +# (c) 2018, Ansible 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 hashlib +import os +import uuid + +from ansible.errors import AnsibleError +from ansible.module_utils.common.text.converters import to_bytes, to_text +from ansible.module_utils.connection import Connection, ConnectionError +from ansible.module_utils.six.moves.urllib.parse import urlsplit +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display + + +display = Display() + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + changed = False + socket_path = None + network_os = self._get_network_os(task_vars).split(".")[-1] + persistent_connection = self._play_context.connection.split(".")[-1] + + result = super(ActionModule, self).run(task_vars=task_vars) + + if persistent_connection != "network_cli": + # It is supported only with network_cli + result["failed"] = True + result["msg"] = ( + "connection type %s is not valid for net_put module," + " please use fully qualified name of network_cli connection type" + % self._play_context.connection + ) + return result + + try: + src = self._task.args["src"] + except KeyError as exc: + return { + "failed": True, + "msg": "missing required argument: %s" % exc, + } + + src_file_path_name = src + + # Get destination file if specified + dest = self._task.args.get("dest") + + # Get proto + proto = self._task.args.get("protocol") + if proto is None: + proto = "scp" + + # Get mode if set + mode = self._task.args.get("mode") + if mode is None: + mode = "binary" + + if mode == "text": + try: + self._handle_src_option(convert_data=False) + except AnsibleError as exc: + return dict(failed=True, msg=to_text(exc)) + + # Now src has resolved file write to disk in current diectory for scp + src = self._task.args.get("src") + filename = str(uuid.uuid4()) + cwd = self._loader.get_basedir() + output_file = os.path.join(cwd, filename) + try: + with open(output_file, "wb") as f: + f.write(to_bytes(src, encoding="utf-8")) + except Exception: + os.remove(output_file) + raise + else: + try: + output_file = self._get_binary_src_file(src) + except ValueError as exc: + return dict(failed=True, msg=to_text(exc)) + + if socket_path is None: + socket_path = self._connection.socket_path + + conn = Connection(socket_path) + sock_timeout = conn.get_option("persistent_command_timeout") + + if dest is None: + dest = src_file_path_name + try: + changed = self._handle_existing_file(conn, output_file, dest, proto, sock_timeout) + if changed is False: + result["changed"] = changed + result["destination"] = dest + if mode == "text": + # Cleanup tmp file expanded wih ansible vars + os.remove(output_file) + return result + except Exception as exc: + result["msg"] = "Warning: %s idempotency check failed. Check dest" % exc + + try: + conn.copy_file( + source=output_file, + destination=dest, + proto=proto, + timeout=sock_timeout, + ) + except Exception as exc: + if to_text(exc) == "No response from server": + if network_os == "iosxr": + # IOSXR sometimes closes socket prematurely after completion + # of file transfer + result["msg"] = "Warning: iosxr scp server pre close issue. Please check dest" + else: + result["failed"] = True + result["msg"] = "Exception received: %s" % exc + + if mode == "text": + # Cleanup tmp file expanded wih ansible vars + os.remove(output_file) + + result["changed"] = changed + result["destination"] = dest + return result + + def _handle_existing_file(self, conn, source, dest, proto, timeout): + """ + Determines whether the source and destination file match. + + :return: False if source and dest both exist and have matching sha1 sums, True otherwise. + """ + cwd = self._loader.get_basedir() + filename = str(uuid.uuid4()) + tmp_source_file = os.path.join(cwd, filename) + try: + conn.get_file( + source=dest, + destination=tmp_source_file, + proto=proto, + timeout=timeout, + ) + except (ConnectionError, AnsibleError) as exc: + error = to_text(exc) + if "No such file or directory" in error or "File doesn't exist" in error: + if os.path.exists(tmp_source_file): + os.remove(tmp_source_file) + return True + raise + try: + with open(source, "r") as f: + new_content = f.read() + with open(tmp_source_file, "r") as f: + old_content = f.read() + except (IOError, OSError): + os.remove(tmp_source_file) + raise + + sha1 = hashlib.sha1() + old_content_b = to_bytes(old_content, errors="surrogate_or_strict") + sha1.update(old_content_b) + checksum_old = sha1.digest() + + sha1 = hashlib.sha1() + new_content_b = to_bytes(new_content, errors="surrogate_or_strict") + sha1.update(new_content_b) + checksum_new = sha1.digest() + os.remove(tmp_source_file) + if checksum_old == checksum_new: + return False + return True + + def _get_binary_src_file(self, src): + working_path = self._get_working_path() + + if os.path.isabs(src) or urlsplit("src").scheme: + source = src + else: + source = self._loader.path_dwim_relative(working_path, "templates", src) + if not source: + source = self._loader.path_dwim_relative(working_path, src) + + if not os.path.exists(source): + raise ValueError("path specified in src not found") + + return source + + def _get_working_path(self): + cwd = self._loader.get_basedir() + if self._task._role is not None: + cwd = self._task._role._role_path + return cwd + + def _handle_src_option(self, convert_data=True): + src = self._task.args.get("src") + working_path = self._get_working_path() + + if os.path.isabs(src) or urlsplit("src").scheme: + source = src + else: + source = self._loader.path_dwim_relative(working_path, "templates", src) + if not source: + source = self._loader.path_dwim_relative(working_path, src) + + if not os.path.exists(source): + raise AnsibleError("path specified in src not found") + + try: + with open(source, "r") as f: + template_data = to_text(f.read()) + except IOError as e: + raise AnsibleError( + "unable to load src file {0}, I/O error({1}): {2}".format( + source, e.errno, e.strerror + ) + ) + + # Create a template search path in the following order: + # [working_path, self_role_path, dependent_role_paths, dirname(source)] + searchpath = [working_path] + if self._task._role is not None: + searchpath.append(self._task._role._role_path) + if hasattr(self._task, "_block:"): + dep_chain = self._task._block.get_dep_chain() + if dep_chain is not None: + for role in dep_chain: + searchpath.append(role._role_path) + searchpath.append(os.path.dirname(source)) + self._templar.environment.loader.searchpath = searchpath + self._task.args["src"] = self._templar.template(template_data) + + def _get_network_os(self, task_vars): + if "network_os" in self._task.args and self._task.args["network_os"]: + display.vvvv("Getting network OS from task argument") + network_os = self._task.args["network_os"] + elif self._play_context.network_os: + display.vvvv("Getting network OS from inventory") + network_os = self._play_context.network_os + elif ( + "network_os" in task_vars.get("ansible_facts", {}) + and task_vars["ansible_facts"]["network_os"] + ): + display.vvvv("Getting network OS from fact") + network_os = task_vars["ansible_facts"]["network_os"] + else: + raise AnsibleError("ansible_network_os must be specified on this host") + + return network_os diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/netconf.py b/collections/ansible_collections/ansible/netcommon/plugins/action/netconf.py new file mode 100644 index 0000000..8e23d43 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/netconf.py @@ -0,0 +1,41 @@ +# +# Copyright 2018 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 + +from ansible.utils.display import Display + +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + module_name = self._task.action.split(".")[-1] + self._config_module = True if module_name == "netconf_config" else False + persistent_connection = self._play_context.connection.split(".")[-1] + warnings = [] + + if persistent_connection != "netconf": + return { + "failed": True, + "msg": "Connection type %s is not valid for %s module. " + "Valid connection type is netconf." % (self._play_context.connection, module_name), + } + + result = super(ActionModule, self).run(task_vars=task_vars) + if warnings: + if "warnings" in result: + result["warnings"].extend(warnings) + else: + result["warnings"] = warnings + return result diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/network.py b/collections/ansible_collections/ansible/netcommon/plugins/action/network.py new file mode 100644 index 0000000..62dd5a9 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/network.py @@ -0,0 +1,429 @@ +# +# (c) 2018 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 os +import re +import time + +from ansible.errors import AnsibleError +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.six.moves.urllib.parse import urlsplit +from ansible.plugins.action.normal import ActionModule as _ActionModule +from ansible.utils.display import Display +from ansible.utils.hashing import checksum, checksum_s + + +# Ansible 2.19+ requires trust_as_template for Jinja2 processing +try: + from ansible.template import trust_as_template + + HAS_TRUST_AS_TEMPLATE = True +except ImportError: + HAS_TRUST_AS_TEMPLATE = False + +display = Display() + + +def _netcommon_remove_internal_keys_fallback(data): + """Mirror ansible.vars.clean.remove_internal_keys when that API is unavailable.""" + from ansible import constants as _ansible_constants + + # ansible-core may drop INTERNAL_RESULT_KEYS from constants; match historical defaults. + _internal_result_keys = getattr( + _ansible_constants, "INTERNAL_RESULT_KEYS", ("add_host", "add_group") + ) + + for key in list(data.keys()): + if ( + key.startswith("_ansible_") and key != "_ansible_parsed" + ) or key in _internal_result_keys: + display.warning( + "Removed unexpected internal key in module return: %s = %s" % (key, data[key]) + ) + del data[key] + + for key in ("warnings", "deprecations"): + if key in data and not data[key]: + del data[key] + + ansible_facts = data.get("ansible_facts") + if ansible_facts: + for key in list(ansible_facts.keys()): + if key.startswith("discovered_interpreter_") or key.startswith( + "ansible_discovered_interpreter_" + ): + del ansible_facts[key] + + +# ansible-core devel may relocate remove_internal_keys; keep a local fallback. +try: + from ansible.vars.clean import remove_internal_keys as _remove_internal_keys +except ImportError: + _remove_internal_keys = _netcommon_remove_internal_keys_fallback + +DEXEC_PREFIX = "ANSIBLE_NETWORK_IMPORT_MODULES:" + + +class ActionModule(_ActionModule): + def run(self, tmp=None, task_vars=None): + config_module = hasattr(self, "_config_module") and self._config_module + if config_module and self._task.args.get("src"): + try: + self._handle_src_option() + except AnsibleError as exc: + return dict(failed=True, msg=to_text(exc)) + + host = task_vars["ansible_host"] + dexec_eligible = self._check_dexec_eligibility(host) + + # attempt to run using dexec + if dexec_eligible: + # find and load the module + filename, module = self._find_load_module() + display.vvvv( + "{prefix} found {action} at {fname}".format( + prefix=DEXEC_PREFIX, + action=self._task.action, + fname=filename, + ), + host, + ) + # not using AnsibleModule, return to normal run (eg eos_bgp) + if getattr(module, "AnsibleModule", None): + # patch and update the module + self._patch_update_module(module, task_vars, host) + display.vvvv( + "{prefix} running {module}".format( + prefix=DEXEC_PREFIX, module=self._task.action + ), + host, + ) + # execute the module, collect result + result = self._exec_module(module) + display.vvvv("{prefix} complete".format(prefix=DEXEC_PREFIX), host) + display.vvvvv( + "{prefix} Result: {result}".format(prefix=DEXEC_PREFIX, result=result), + host, + ) + + else: + dexec_eligible = False + display.vvvv( + "{prefix} {module} doesn't support direct execution, disabled".format( + prefix=DEXEC_PREFIX, module=self._task.action + ), + host, + ) + + if not dexec_eligible: + result = super(ActionModule, self).run(task_vars=task_vars) + + if config_module and self._task.args.get("backup") and not result.get("failed"): + self._handle_backup_option( + result, + task_vars, + self._task.args.get("backup_options"), + ) + + return result + + def _handle_backup_option(self, result, task_vars, backup_options): + filename = None + backup_path = None + try: + non_config_regexes = self._connection.cliconf.get_option("non_config_lines", task_vars) + except (AttributeError, KeyError): + non_config_regexes = [] + try: + content = self._sanitize_contents( + contents=result.pop("__backup__"), filters=non_config_regexes + ) + except KeyError: + raise AnsibleError("Failed while reading configuration backup") + + if backup_options: + filename = backup_options.get("filename") + backup_path = backup_options.get("dir_path") + + tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time())) + if not backup_path: + cwd = self._get_working_path() + backup_path = os.path.join(cwd, "backup") + if not filename: + filename = "%s_config.%s" % ( + task_vars["inventory_hostname"], + tstamp, + ) + + dest = os.path.join(backup_path, filename) + if not os.path.exists(backup_path): + os.makedirs(backup_path) + + changed = False + # Do not overwrite the destination if the contents match. + if not os.path.exists(dest) or checksum(dest) != checksum_s(content): + try: + with open(dest, "w") as output_file: + output_file.write(content) + except Exception as exc: + result["failed"] = True + result["msg"] = "Could not write to destination file %s: %s" % ( + dest, + to_text(exc), + ) + return + changed = True + + result["backup_path"] = dest + result["changed"] = changed + + result["date"], result["time"] = tstamp.split("@") + if not (backup_options and backup_options.get("filename")): + result["filename"] = os.path.basename(result["backup_path"]) + result["shortname"] = os.path.splitext(result["backup_path"])[0] + + def _get_working_path(self): + cwd = self._loader.get_basedir() + if self._task._role is not None: + cwd = self._task._role._role_path + return cwd + + def _handle_src_option(self, convert_data=True): + src = self._task.args.get("src") + working_path = self._get_working_path() + + if os.path.isabs(src) or urlsplit(src).scheme: + source = src + else: + source = self._loader.path_dwim_relative(working_path, "templates", src) + if not source: + source = self._loader.path_dwim_relative(working_path, src) + + if not os.path.exists(source): + raise AnsibleError("path specified in src not found") + + try: + with open(source, "r") as f: + template_data = to_text(f.read()) + except IOError as e: + raise AnsibleError( + "unable to load src file {0}, I/O error({1}): {2}".format( + source, e.errno, e.strerror + ) + ) + + # Create a template search path in the following order: + # [working_path, self_role_path, dependent_role_paths, dirname(source)] + searchpath = [working_path] + if self._task._role is not None: + searchpath.append(self._task._role._role_path) + if hasattr(self._task, "_block:"): + dep_chain = self._task._block.get_dep_chain() + if dep_chain is not None: + for role in dep_chain: + searchpath.append(role._role_path) + searchpath.append(os.path.dirname(source)) + self._templar.environment.loader.searchpath = searchpath + # help_text="Use `ansible.builtin.template` instead.", supported after 2.18 update as per support + display.deprecated( + msg="Direct processing of templates via `src` is deprecated, use `ansible.builtin.template` instead.", + date="2028-01-01", + collection_name="ansible.netcommon", + ) + # Ansible 2.19+ requires marking template data as trusted for Jinja2 processing + # In earlier versions, template data is processed directly + if HAS_TRUST_AS_TEMPLATE: + template_data = trust_as_template(template_data) + + self._task.args["src"] = self._templar.template(template_data) + + def _get_network_os(self, task_vars): + if "network_os" in self._task.args and self._task.args["network_os"]: + display.vvvv("Getting network OS from task argument") + network_os = self._task.args["network_os"] + elif self._play_context.network_os: + display.vvvv("Getting network OS from inventory") + network_os = self._play_context.network_os + elif ( + "network_os" in task_vars.get("ansible_facts", {}) + and task_vars["ansible_facts"]["network_os"] + ): + display.vvvv("Getting network OS from fact") + network_os = task_vars["ansible_facts"]["network_os"] + else: + raise AnsibleError("ansible_network_os must be specified on this host") + + return network_os + + def _check_dexec_eligibility(self, host): + """Check if current python and task are eligble""" + dexec = self.get_connection_option("import_modules") + + # log early about dexec + if dexec: + display.vvvv("{prefix} enabled".format(prefix=DEXEC_PREFIX), host) + + # disable dexec when running async + if self._task.async_val: + dexec = False + display.vvvv( + "{prefix} disabled for a task using async".format(prefix=DEXEC_PREFIX), + host=host, + ) + else: + display.vvvv("{prefix} disabled".format(prefix=DEXEC_PREFIX), host) + display.vvvv( + "{prefix} module execution time may be extended".format(prefix=DEXEC_PREFIX), + host, + ) + + return dexec + + def _find_load_module(self): + """Use the task action to find a module + and import it. + + :return filename: The module's filename + :rtype filename: str + :return module: The loaded module file + :rtype module: module + """ + import importlib + + mloadr = self._shared_loader_obj.module_loader + + # 2.10 + try: + context = mloadr.find_plugin_with_context( + self._task.action, collection_list=self._task.collections + ) + filename = context.plugin_resolved_path + module = importlib.import_module(context.plugin_resolved_name) + # 2.9 + except AttributeError: + fullname, filename = mloadr.find_plugin_with_name( + self._task.action, collection_list=self._task.collections + ) + module = importlib.import_module(fullname) + return filename, module + + def _patch_update_module(self, module, task_vars, host): + """Update a module instance, replacing it's AnsibleModule + with one that doesn't load params + + :param module: An loaded module + :type module: A module file that was loaded + :param task_vars: The vars provided to the task + :type task_vars: dict + """ + import copy + + from ansible.module_utils.basic import AnsibleModule as _AnsibleModule + + # build an AnsibleModule that doesn't load params + class DirectExecutionModule(_AnsibleModule): + def _load_params(self): + """Don't load params for action plugin use case - params set externally""" + display.vvvv( + "{prefix} _load_params skipped for action plugin in direct execution".format( + prefix=DEXEC_PREFIX + ), + host, + ) + pass + + def _record_module_result(self, o): + """Override new 2.19.1+ hook to directly record the module result as a module attr.""" + module._raw_result = o + + # update the task args w/ all the magic vars + self._update_module_args(self._task.action, self._task.args, task_vars) + + # set the params of the ansible module cause we're not using stdin + # use a copy so the module doesn't change the original task args + DirectExecutionModule.params = copy.deepcopy(self._task.args) + + # give the module our revised AnsibleModule + module.AnsibleModule = DirectExecutionModule + + def _exec_module(self, module): + """exec the module's main() since modules + print their result, we need to replace stdout + with a buffer. If main() fails, we assume that as stderr + Once we collect stdout/stderr, use our super to json load + it or handle a traceback + + :param module: An loaded module + :type module: A module file that was loaded + :return module_result: The result of the module + :rtype module_result: dict + """ + import io + import sys + + # preserve stdout/stderr to swap and restore later, create private buffers to capture + sys_stdout = sys.stdout + sys_stderr = sys.stderr + captured_stdout = io.StringIO() + captured_stderr = io.StringIO() + + try: + # temporarily swap stdout/stderr with private buffers + sys.stdout = captured_stdout + sys.stderr = captured_stderr + + module.main() # allow unhandled module exceptions to fly; handled by TE to preserve as much error detail as possible + except SystemExit: + pass # module exited cleanly + finally: + # restore stdout/stderr + sys.stdout = sys_stdout + sys.stderr = sys_stderr + + try: + # if 2.19.1+, anything using fail_json/exit_json from the patched module should have recorded _raw_result + data = module._raw_result + except AttributeError: + # if _raw_result is not available, results should be on stdout/stderr + stdout = captured_stdout.getvalue() + stderr = captured_stderr.getvalue() + + dict_out = { + "stdout": stdout, + "stdout_lines": stdout.splitlines(), + "stderr": stderr, + "stderr_lines": stderr.splitlines(), + } + + # parse the response + data = self._parse_returned_data(dict_out) + + # Clean up the response like action _execute_module + _remove_internal_keys(data) + + # split stdout/stderr into lines if needed + if "stdout" in data and "stdout_lines" not in data: + # if the value is 'False', a default won't catch it. + txt = data.get("stdout", None) or "" + data["stdout_lines"] = txt.splitlines() + if "stderr" in data and "stderr_lines" not in data: + # if the value is 'False', a default won't catch it. + txt = data.get("stderr", None) or "" + data["stderr_lines"] = txt.splitlines() + + return data + + def _sanitize_contents(self, contents, filters): + """remove lines from contents that match + regexes specified in the `filters` list + """ + for x in filters: + contents = re.sub(x, "", contents) + return contents.strip() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/network_resource.py b/collections/ansible_collections/ansible/netcommon/plugins/action/network_resource.py new file mode 100644 index 0000000..54ecf68 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/network_resource.py @@ -0,0 +1,246 @@ +# +# Copyright 2021 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 copy +import glob +import os + +from importlib import import_module + + +try: + import yaml + + # use C version if possible for speedup + try: + from yaml import CSafeLoader as SafeLoader + except ImportError: + from yaml import SafeLoader + HAS_YAML = True +except ImportError: + HAS_YAML = False + +from ansible.errors import AnsibleActionFail, AnsibleError +from ansible.module_utils.common.text.converters import to_text +from ansible.utils.display import Display + +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +display = Display() + + +class RunMode: + RM_LIST = 0 # get list of supported resource modules for given os_name + RM_GET = 1 # get resource module facts for given host + RM_CONFIG = 1 # push resource module configuration + + +class ActionModule(ActionNetworkModule): + def run(self, task_vars=None): + run_mode = None + self._task_vars = task_vars + + self._rm_play_context = copy.deepcopy(self._play_context) + self._os_name = self._task.args.get("os_name") or self._get_os_name() + if not self._os_name: + return {"error": "either value of 'os_name' or 'ansible_network_os' should be set"} + + if len(self._os_name.split(".")) != 3: + msg = ( + "OS value name should a fully qualified collection name in the format" + " .." + ) + return {"error": msg} + + self._rm_play_context.network_os = self._os_name + + self._name = self._task.args.get("name") + self._config = self._task.args.get("config") + self._running_config = self._task.args.get("running_config") + self._state = self._task.args.get("state") + + run_mode = self._get_run_mode() + + result = {} + if run_mode == RunMode.RM_LIST: + result = self._list_resource_modules() + elif run_mode in [RunMode.RM_GET, RunMode.RM_CONFIG]: + try: + result = self._run_resource_module() + except AnsibleError as exc: + # handle short name redirection not working for ansible-2.9 + if "was not found" in to_text(exc): + result = self._run_resource_module(prefix_os_name=True) + else: + raise + + result.update( + { + "ansible_network_os": self._task_vars.get("ansible_network_os"), + "ansible_connection": self._task_vars.get("ansible_connection"), + } + ) + return result + + def _run_resource_module(self, prefix_os_name=False): + new_task = self._task.copy() + + self._module = self._get_resource_module(prefix_os_name=prefix_os_name) + if not self._module: + msg = "Could not find resource module '%s' for os name '%s'" % ( + self._name, + self._os_name, + ) + raise AnsibleActionFail(msg) + + new_task.action = self._module + action = self._shared_loader_obj.action_loader.get( + self._rm_play_context.network_os, + task=new_task, + connection=self._connection, + play_context=self._rm_play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj, + ) + display.vvvv("Running resource module %s" % self._module) + for option in ["os_name", "name"]: + if option in new_task.args: + new_task.args.pop(option) + + result = action.run(task_vars=self._task_vars) + result.update({"resource_module_name": self._module}) + return result + + def _get_resource_module(self, prefix_os_name=False): + if "." in self._name: + if len(self._name.split(".")) != 3: + msg = ( + "name should a fully qualified collection name in the format" + " .." + ) + raise AnsibleActionFail(msg) + fqcn_module_name = self._name + else: + if prefix_os_name: + module_name = self._os_name.split(".")[1] + "_" + self._name + else: + module_name = self._name + + fqcn_module_name = ".".join(self._os_name.split(".")[:2] + [module_name]) + + return fqcn_module_name + + def _get_os_name(self): + os_name = None + if "network_os" in self._task.args and self._task.args["network_os"]: + display.vvvv("Getting OS name from task argument") + os_name = self._task.args["network_os"] + elif self._play_context.network_os: + display.vvvv("Getting OS name from inventory") + os_name = self._play_context.network_os + elif ( + "network_os" in self._task_vars.get("ansible_facts", {}) + and self._task_vars["ansible_facts"]["network_os"] + ): + display.vvvv("Getting OS name from fact") + os_name = self._task_vars["ansible_facts"]["network_os"] + + return os_name + + def _is_resource_module(self, docs): + doc_obj = yaml.load(docs, SafeLoader) + if "config" in doc_obj["options"] and "state" in doc_obj["options"]: + return True + + def _get_run_mode(self): + error_msg = None + if self._config or self._running_config: + if not self._name: + error_msg = "'name' is required if 'config' option is set" + if not self._state: + error_msg = "'state' is required if 'config' option is set" + run_mode = RunMode.RM_CONFIG + elif self._state: + if not self._name: + error_msg = "'name' is required if 'state' option is set" + run_mode = RunMode.RM_GET + elif self._name: + if not any([self._config, self._running_config, self._state]): + error_msg = ( + "If 'name' is set atleast one of 'config', " + "'running_config' or 'state' is required" + ) + else: + run_mode = RunMode.RM_LIST + + if error_msg: + raise AnsibleActionFail(error_msg) + return run_mode + + def _list_resource_modules(self): + result = {} + resource_modules = [] + + self._cref = dict(zip(["corg", "cname", "plugin"], self._os_name.split("."))) + + fact_modulelib = "ansible_collections.{corg}.{cname}.plugins.module_utils.network.{plugin}.facts.facts".format( + corg=self._cref["corg"], + cname=self._cref["cname"], + plugin=self._cref["plugin"], + ) + + try: + display.vvvv("fetching facts list from path %s" % (fact_modulelib)) + facts_resource_subset = getattr(import_module(fact_modulelib), "FACT_RESOURCE_SUBSETS") + resource_modules = sorted(facts_resource_subset.keys()) + except ModuleNotFoundError: + display.vvvv("'%s' is not defined" % (fact_modulelib)) + except AttributeError: + display.vvvv("'FACT_RESOURCE_SUBSETS is not defined in '%s'" % (fact_modulelib)) + + # parse module docs to check for 'config' and 'state' options to identify it as resource module + if not resource_modules: + modulelib = "ansible_collections.{corg}.{cname}.plugins.modules".format( + corg=self._cref["corg"], cname=self._cref["cname"] + ) + + module_dir_path = os.path.dirname(import_module(modulelib).__file__) + module_paths = glob.glob( + "{module_dir_path}/[!_]*.py".format(module_dir_path=module_dir_path) + ) + + for module_path in module_paths: + module_name = os.path.basename(module_path).split(".")[0] + docs = None + try: + display.vvvv("reading 'DOCUMENTATION' from path %s" % (module_path)) + docs = getattr( + import_module("%s.%s" % (modulelib, module_name)), + "DOCUMENTATION", + ) + except ModuleNotFoundError: + display.vvvv("'%s' is not defined" % (fact_modulelib)) + except AttributeError: + display.vvvv("'DOCUMENTATION is not defined in '%s'" % (fact_modulelib)) + + if docs: + if self._is_resource_module(docs): + resource_modules.append(module_name.split("_", 1)[1]) + else: + display.vvvvv( + "module in path '%s' is not a resource module" % (module_path) + ) + + result.update({"modules": sorted(resource_modules)}) + return result diff --git a/collections/ansible_collections/ansible/netcommon/plugins/action/telnet.py b/collections/ansible_collections/ansible/netcommon/plugins/action/telnet.py new file mode 100644 index 0000000..c5072a7 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/action/telnet.py @@ -0,0 +1,118 @@ +# (c) 2017, 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 + +from time import sleep + +from ansible.module_utils.common.text.converters import to_bytes, to_text +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.compat import telnetlib + + +text_type = str +display = Display() + + +class ActionModule(ActionBase): + TRANSFERS_FILES = False + + def run(self, tmp=None, task_vars=None): + if self._task.environment and any(self._task.environment): + self._display.warning("The telnet task does not support the environment keyword") + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + if self._play_context.check_mode: + # in --check mode, always skip this module execution + result["skipped"] = True + result["msg"] = "The telnet task does not support check mode" + else: + result["changed"] = True + result["failed"] = False + + host = to_text(self._task.args.get("host", self._play_context.remote_addr)) + user = to_text(self._task.args.get("user", self._play_context.remote_user)) + password = to_text(self._task.args.get("password", self._play_context.password)) + + # FIXME, default to play_context? + port = int(self._task.args.get("port", 23)) + timeout = int(self._task.args.get("timeout", 120)) + pause = int(self._task.args.get("pause", 1)) + + send_newline = self._task.args.get("send_newline", False) + crlf = self._task.args.get("crlf", False) + + login_prompt = to_text(self._task.args.get("login_prompt", "login: ")) + password_prompt = to_text(self._task.args.get("password_prompt", "Password: ")) + prompts = self._task.args.get("prompts", ["\\$ "]) + commands = self._task.args.get("command") or self._task.args.get("commands") + + if crlf: + line_ending = "\r\n" + else: + line_ending = "\n" + + if isinstance(commands, text_type): + commands = commands.split(",") + + if isinstance(commands, list) and commands: + self.tn = telnetlib.Telnet(host, port, timeout) + + self.output = bytes() + try: + if send_newline: + self.tn.write(to_bytes(line_ending)) + + self.await_prompts([login_prompt], timeout) + display.vvvvv(">>>user: %s" % user) + self.tn.write(to_bytes(user + line_ending)) + + if password: + self.await_prompts([password_prompt], timeout) + display.vvvvv(">>>password: %s" % password) + self.tn.write(to_bytes(password + line_ending)) + + self.await_prompts(prompts, timeout) + + for cmd in commands: + display.vvvvv(">>> %s" % cmd) + self.tn.write(to_bytes(cmd + line_ending)) + self.await_prompts(prompts, timeout) + display.vvvvv("<<< %s" % cmd) + sleep(pause) + + self.tn.write(to_bytes("exit" + line_ending)) + + except EOFError as e: + result["failed"] = True + result["msg"] = "Telnet action failed: %s" % to_text(e) + except TimeoutError as e: + result["failed"] = True + result["msg"] = "Telnet timed out trying to find prompt(s): '%s'" % to_text(e) + finally: + if self.tn: + self.tn.close() + result["stdout"] = to_text(self.output) + result["stdout_lines"] = self.output.splitlines(True) + else: + result["failed"] = True + result["msg"] = "Telnet requires a command to execute" + + return result + + def await_prompts(self, prompts, timeout): + index, match, out = self.tn.expect(list(map(to_bytes, prompts)), timeout=timeout) + self.output += out + if not match: + raise TimeoutError(prompts) + + return index diff --git a/collections/ansible_collections/ansible/netcommon/plugins/become/enable.py b/collections/ansible_collections/ansible/netcommon/plugins/become/enable.py new file mode 100644 index 0000000..1ad4c3d --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/become/enable.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2018, 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 = """ +author: + - Ansible Networking Team (@ansible-network) +name: enable +short_description: Switch to elevated permissions on a network device +description: +- This become plugins allows elevated permissions on a remote network device. +version_added: 1.0.0 +options: + become_pass: + description: password + ini: + - section: enable_become_plugin + key: password + vars: + - name: ansible_become_password + - name: ansible_become_pass + - name: ansible_enable_pass + env: + - name: ANSIBLE_BECOME_PASS + - name: ANSIBLE_ENABLE_PASS +notes: +- enable is really implemented in the network connection handler and as such can only + be used with network connections. +- This plugin ignores the 'become_exe' and 'become_user' settings as it uses an API + and not an executable. +""" + +from ansible.plugins.become import BecomeBase + + +class BecomeModule(BecomeBase): + name = "ansible.netcommon.enable" + + def build_become_command(self, cmd, shell): + # enable is implemented inside the network connection plugins + return cmd diff --git a/collections/ansible_collections/ansible/netcommon/plugins/cache/memory.py b/collections/ansible_collections/ansible/netcommon/plugins/cache/memory.py new file mode 100644 index 0000000..5dcf3c1 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/cache/memory.py @@ -0,0 +1,49 @@ +# +# (c) 2020 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 = """ + short_description: RAM backed, non persistent cache. + description: + - RAM backed cache that is not persistent. + - Tailored for networking use case. + version_added: 2.0.0 + author: + - Ansible Networking Team (@ansible-network) + name: memory +""" + +from ansible.plugins import AnsiblePlugin + + +class CacheModule(AnsiblePlugin): + def __init__(self, *args, **kwargs): + super(CacheModule, self).__init__(*args, **kwargs) + self._cache = {} + + def get(self, key): + return self._cache.get(key) + + def set(self, key, value): + self._cache[key] = value + + def keys(self): + return self._cache.keys() + + def flush(self): + self._cache = {} + + def lookup(self, key): + return self.get(key) + + def populate(self, key, value): + self.set(key, value) + + def invalidate(self): + self.flush() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/cliconf/default.py b/collections/ansible_collections/ansible/netcommon/plugins/cliconf/default.py new file mode 100644 index 0000000..5d418d5 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/cliconf/default.py @@ -0,0 +1,67 @@ +# (c) 2023 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: Ansible Networking Team (@ansible-network) +name: default +short_description: General purpose cliconf plugin for new platforms +description: +- This plugin attemts to provide low level abstraction apis for sending and receiving CLI + commands from arbitrary network devices. +version_added: 5.2.0 +""" + +import json + +from ansible.errors import AnsibleConnectionFailure + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.cliconf_base import CliconfBase + + +class Cliconf(CliconfBase): + def __init__(self, *args, **kwargs): + super(Cliconf, self).__init__(*args, **kwargs) + self._device_info = {} + + def get_device_info(self): + if not self._device_info: + device_info = {} + + device_info["network_os"] = "default" + self._device_info = device_info + + return self._device_info + + def get_config(self, flags=None, format=None): + network_os = self.get_device_info()["network_os"] + raise AnsibleConnectionFailure("get_config is not supported by network_os %s" % network_os) + + def edit_config(self, candidate=None, commit=True, replace=None, comment=None): + network_os = self.get_device_info()["network_os"] + raise AnsibleConnectionFailure("edit_config is not supported by network_os %s" % network_os) + + def get_capabilities(self): + result = super(Cliconf, self).get_capabilities() + result["device_operations"] = self.get_device_operations() + return json.dumps(result) + + def get_device_operations(self): + return { + "supports_diff_replace": False, + "supports_commit": False, + "supports_rollback": False, + "supports_defaults": False, + "supports_onbox_diff": False, + "supports_commit_comment": False, + "supports_multiline_delimiter": False, + "supports_diff_match": False, + "supports_diff_ignore_lines": False, + "supports_generate_diff": False, + "supports_replace": False, + } diff --git a/collections/ansible_collections/ansible/netcommon/plugins/connection/grpc.py b/collections/ansible_collections/ansible/netcommon/plugins/connection/grpc.py new file mode 100644 index 0000000..b44baaf --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/connection/grpc.py @@ -0,0 +1,278 @@ +# (c) 2022 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 = """ +author: + - Ansible Networking Team (@ansible-network) +name: grpc +short_description: Provides a persistent connection using the gRPC protocol +description: + - This connection plugin provides a connection to remote devices over gRPC and + is typically used with devices for sending and receiving RPC calls + over gRPC framework. + - Note this connection plugin requires the grpcio python library to be installed on the + local Ansible controller. +version_added: "3.1.0" +requirements: + - grpcio + - protobuf +extends_documentation_fragment: + - ansible.netcommon.connection_persistent +options: + host: + description: + - Specifies the remote device FQDN or IP address to establish the gRPC + connection to. + default: inventory_hostname + type: string + vars: + - name: ansible_host + port: + type: int + description: + - Specifies the port on the remote device that listens for connections + when establishing the gRPC connection. If None only the C(host) part will + be used. + ini: + - section: defaults + key: remote_port + env: + - name: ANSIBLE_REMOTE_PORT + vars: + - name: ansible_port + network_os: + description: + - Configures the device platform network operating system. This value is + used to load a device specific grpc plugin to communicate with the remote + device. + type: string + vars: + - name: ansible_network_os + remote_user: + description: + - The username used to authenticate to the remote device when the gRPC + connection is first established. If the remote_user is not specified, + the connection will use the username of the logged in user. + - Can be configured from the CLI via the C(--user) or C(-u) options. + type: string + ini: + - section: defaults + key: remote_user + env: + - name: ANSIBLE_REMOTE_USER + vars: + - name: ansible_user + password: + description: + - Configures the user password used to authenticate to the remote device + when first establishing the gRPC connection. + type: string + vars: + - name: ansible_password + - name: ansible_ssh_pass + private_key_file: + description: + - The PEM encoded private key file used to authenticate to the + remote device when first establishing the grpc connection. + type: string + ini: + - section: grpc_connection + key: private_key_file + env: + - name: ANSIBLE_PRIVATE_KEY_FILE + vars: + - name: ansible_private_key_file + root_certificates_file: + description: + - The PEM encoded root certificate file used to create a SSL-enabled + channel, if the value is None it reads the root certificates from + a default location chosen by gRPC at runtime. + type: string + ini: + - section: grpc_connection + key: root_certificates_file + env: + - name: ANSIBLE_ROOT_CERTIFICATES_FILE + vars: + - name: ansible_root_certificates_file + certificate_chain_file: + description: + - The PEM encoded certificate chain file used to create a SSL-enabled + channel. If the value is None, no certificate chain is used. + type: string + ini: + - section: grpc_connection + key: certificate_chain_file + env: + - name: ANSIBLE_CERTIFICATE_CHAIN_FILE + vars: + - name: ansible_certificate_chain_file + ssl_target_name_override: + description: + - The option overrides SSL target name used for SSL host name checking. + The name used for SSL host name checking will be the target parameter + (assuming that the secure channel is an SSL channel). If this parameter is + specified and the underlying is not an SSL channel, it will just be ignored. + type: string + ini: + - section: grpc_connection + key: ssl_target_name_override + env: + - name: ANSIBLE_GPRC_SSL_TARGET_NAME_OVERRIDE + vars: + - name: ansible_grpc_ssl_target_name_override + grpc_type: + description: + - This option indicates the grpc type and it can be used + in place of network_os. (example cisco.iosxr.iosxr) + default: False + ini: + - section: grpc_connection + key: type + env: + - name: ANSIBLE_GRPC_CONNECTION_TYPE + vars: + - name: ansible_grpc_connection_type +""" + +from importlib import import_module + +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.plugins.connection import NetworkConnectionBase + + +try: + from grpc import insecure_channel, secure_channel, ssl_channel_credentials + from grpc.beta import implementations + + HAS_GRPC = True +except ImportError: + HAS_GRPC = False + +try: + from google import protobuf # noqa: F401 # pylint: disable=unused-import + + HAS_PROTOBUF = True +except ImportError: + HAS_PROTOBUF = False + + +class Connection(NetworkConnectionBase): + """GRPC connections""" + + transport = "ansible.netcommon.grpc" + has_pipelining = False + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + + grpc_type = self._network_os or self.get_option("grpc_type") + if grpc_type: + if not HAS_PROTOBUF: + raise AnsibleError( + "protobuf is required to use the grpc connection type. Please run 'pip install protobuf'" + ) + if not self._network_os: + self._network_os = grpc_type + cref = dict(zip(["corg", "cname", "plugin"], grpc_type.split("."))) + grpclib = "ansible_collections.{corg}.{cname}.plugins.sub_plugins.grpc.{plugin}".format( + **cref + ) + grpccls = getattr(import_module(grpclib), "Grpc") + grpc_obj = grpccls(self) + + if grpc_obj: + self._sub_plugin = { + "type": "grpc", + "name": grpc_type, + "obj": grpc_obj, + } + self.queue_message("log", "loaded gRPC plugin for type %s" % grpc_type) + self.queue_message("log", "grpc type is set to %s" % grpc_type) + else: + raise AnsibleConnectionFailure( + "unable to load API plugin for network_os %s" % grpc_type + ) + else: + raise AnsibleConnectionFailure( + "Unable to automatically determine gRPC implementation type." + " Please manually configure ansible_network_os value or grpc_type configuration for this host", + ) + + def _connect(self): + """ + Create GRPC connection to target host + :return: None + """ + if not HAS_GRPC: + raise AnsibleError( + "grpcio is required to use the gRPC connection type. Please run 'pip install grpcio'" + ) + host = self.get_option("host") + host = self._play_context.remote_addr + if self.connected: + self.queue_message("log", "gRPC connection to host %s already exist" % host) + return + + port = self.get_option("port") + self._target = host if port is None else "%s:%d" % (host, port) + self._timeout = self.get_option("persistent_command_timeout") + self._login_credentials = [ + ("username", self.get_option("remote_user")), + ("password", self.get_option("password")), + ] + ssl_target_name_override = self.get_option("ssl_target_name_override") + if ssl_target_name_override: + self._channel_options = [ + ("grpc.ssl_target_name_override", ssl_target_name_override), + ] + else: + self._channel_options = None + + certs = {} + private_key_file = self.get_option("private_key_file") + root_certificates_file = self.get_option("root_certificates_file") + certificate_chain_file = self.get_option("certificate_chain_file") + + try: + if root_certificates_file: + with open(root_certificates_file, "rb") as f: + certs["root_certificates"] = f.read() + if private_key_file: + with open(private_key_file, "rb") as f: + certs["private_key"] = f.read() + if certificate_chain_file: + with open(certificate_chain_file, "rb") as f: + certs["certificate_chain"] = f.read() + except Exception as e: + raise AnsibleConnectionFailure("Failed to read certificate keys: %s" % e) + if certs: + creds = ssl_channel_credentials(**certs) + channel = secure_channel(self._target, creds, options=self._channel_options) + else: + channel = insecure_channel(self._target, options=self._channel_options) + + self.queue_message( + "vvv", + "ESTABLISH GRPC CONNECTION FOR USER: %s on PORT %s TO %s" + % (self.get_option("remote_user"), port, host), + ) + self._channel = implementations.Channel(channel) + self.queue_message("vvvv", "grpc connection has completed successfully") + self._connected = True + + def close(self): + """ + Close the active session to the device + :return: None + """ + if self._connected: + self.queue_message("vvvv", "closing gRPC connection to target host") + self._channel.close() + super(Connection, self).close() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/connection/httpapi.py b/collections/ansible_collections/ansible/netcommon/plugins/connection/httpapi.py new file mode 100644 index 0000000..1524aa6 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/connection/httpapi.py @@ -0,0 +1,383 @@ +# (c) 2018 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: + - Ansible Networking Team (@ansible-network) +name: httpapi +short_description: Use httpapi to run command on network appliances +description: +- This connection plugin provides a connection to remote devices over a HTTP(S)-based + api. +version_added: 1.0.0 +extends_documentation_fragment: +- ansible.netcommon.connection_persistent +options: + host: + description: + - Specifies the remote device FQDN or IP address to establish the HTTP(S) connection + to. + default: inventory_hostname + type: string + vars: + - name: inventory_hostname + - name: ansible_host + port: + type: int + description: + - Specifies the port on the remote device that listens for connections when establishing + the HTTP(S) connection. + - When unspecified, will pick 80 or 443 based on the value of use_ssl. + ini: + - section: defaults + key: remote_port + env: + - name: ANSIBLE_REMOTE_PORT + vars: + - name: ansible_httpapi_port + network_os: + description: + - Configures the device platform network operating system. This value is used + to load the correct httpapi plugin to communicate with the remote device + type: string + vars: + - name: ansible_network_os + remote_user: + description: + - The username used to authenticate to the remote device when the API connection + is first established. If the remote_user is not specified, the connection will + use the username of the logged in user. + - Can be configured from the CLI via the C(--user) or C(-u) options. + type: string + ini: + - section: defaults + key: remote_user + env: + - name: ANSIBLE_REMOTE_USER + vars: + - name: ansible_user + password: + description: + - Configures the user password used to authenticate to the remote device when + needed for the device API. + type: string + vars: + - name: ansible_password + - name: ansible_httpapi_pass + - name: ansible_httpapi_password + session_key: + type: dict + description: + - Configures the session key to be used to authenticate to the remote device when + needed for the device API. + - This should contain a dictionary representing the key name and value for the + token. + - When specified, I(password) is ignored. + vars: + - name: ansible_httpapi_session_key + ca_path: + description: + - Path to CA cert bundle to use. + type: path + version_added: 5.2.0 + vars: + - name: ansible_httpapi_ca_path + client_cert: + description: + - PEM formatted certificate chain file to be used for SSL client + authentication. This file can also include the key as well, and if the key + is included, I(client_key) is not required + version_added: 5.2.0 + vars: + - name: ansible_httpapi_client_cert + client_key: + description: + - PEM formatted file that contains the private key to be used for SSL client + authentication. If I(client_cert) contains both the certificate and key, + this option is not required. + version_added: 5.2.0 + vars: + - name: ansible_httpapi_client_key + http_agent: + description: User-Agent to use in the request. + version_added: 5.2.0 + vars: + - name: ansible_httpapi_http_agent + use_ssl: + type: boolean + description: + - Whether to connect using SSL (HTTPS) or not (HTTP). + default: false + vars: + - name: ansible_httpapi_use_ssl + validate_certs: + type: boolean + description: + - Whether to validate SSL certificates + default: true + vars: + - name: ansible_httpapi_validate_certs + use_proxy: + type: boolean + description: + - Whether to use https_proxy for requests. + default: true + vars: + - name: ansible_httpapi_use_proxy + ciphers: + description: + - SSL/TLS Ciphers to use for requests + - 'When a list is provided, all ciphers are joined in order with C(:)' + - See the L(OpenSSL Cipher List Format,https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT) + for more details. + - The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions. + - This option will have no effect on ansible-core<2.14 but a warning will be emitted. + version_added: 5.0.0 + type: list + elements: string + vars: + - name: ansible_httpapi_ciphers + become: + type: boolean + description: + - The become option will instruct the CLI session to attempt privilege escalation + on platforms that support it. Normally this means transitioning from user mode + to C(enable) mode in the CLI session. If become is set to True and the remote + device does not support privilege escalation or the privilege has already been + elevated, then this option is silently ignored. + - Can be configured from the CLI via the C(--become) or C(-b) options. + default: false + ini: + - section: privilege_escalation + key: become + env: + - name: ANSIBLE_BECOME + vars: + - name: ansible_become + become_method: + description: + - This option allows the become method to be specified in for handling privilege + escalation. Typically the become_method value is set to C(enable) but could + be defined as other values. + default: sudo + type: string + ini: + - section: privilege_escalation + key: become_method + env: + - name: ANSIBLE_BECOME_METHOD + vars: + - name: ansible_become_method + platform_type: + description: + - Set type of platform. + type: string + env: + - name: ANSIBLE_PLATFORM_TYPE + vars: + - name: ansible_platform_type +""" + +from io import BytesIO + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils.common.text.converters import to_bytes +from ansible.module_utils.six.moves import cPickle +from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError +from ansible.module_utils.urls import open_url +from ansible.playbook.play_context import PlayContext +from ansible.plugins.connection import ensure_connect +from ansible.plugins.loader import httpapi_loader +from ansible.release import __version__ as ANSIBLE_CORE_VERSION + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.connection_base import ( + NetworkConnectionBase, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.version import Version + + +class Connection(NetworkConnectionBase): + """Network API connection""" + + transport = "ansible.netcommon.httpapi" + has_pipelining = True + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + + self._auth = None + if self._network_os: + self.load_platform_plugins(self._network_os) + + def load_platform_plugins(self, platform_type=None): + platform_type = platform_type or self.get_option("platform_type") + + if platform_type: + self.httpapi = httpapi_loader.get(platform_type, self) + if self.httpapi: + self._sub_plugin = { + "type": "httpapi", + "name": self.httpapi._load_name, + "obj": self.httpapi, + } + self.queue_message( + "vvvv", + "loaded API plugin %s from path %s for platform type %s" + % ( + self.httpapi._load_name, + self.httpapi._original_path, + platform_type, + ), + ) + else: + raise AnsibleConnectionFailure( + "unable to load API plugin for platform type %s" % platform_type + ) + + else: + raise AnsibleConnectionFailure( + "Unable to automatically determine host platform type. Please " + "manually configure platform_type value for this host" + ) + self.queue_message("log", "platform_type is set to %s" % platform_type) + + @property + def _url(self): + protocol = "https" if self.get_option("use_ssl") else "http" + host = self.get_option("host") + port = self.get_option("port") or (443 if protocol == "https" else 80) + return "%s://%s:%s" % (protocol, host, port) + + def update_play_context(self, pc_data): + """Updates the play context information for the connection""" + pc_data = to_bytes(pc_data) + pc_data = cPickle.loads(pc_data, encoding="bytes") + + play_context = PlayContext() + play_context.deserialize(pc_data) + + self.queue_message("vvvv", "updating play_context for connection") + if self._play_context.become ^ play_context.become: + self.set_become(play_context) + if play_context.become is True: + self.queue_message("vvvv", "authorizing connection") + else: + self.queue_message("vvvv", "deauthorizing connection") + + self._play_context = play_context + + def _connect(self): + if not self.connected: + self.queue_message( + "vvv", + "ESTABLISH HTTP(S) CONNECTFOR USER: %s TO %s" + % (self._play_context.remote_user, self._url), + ) + self.httpapi.set_become(self._play_context) + self._connected = True + + if self.get_option("session_key"): + self._auth = self.get_option("session_key") + else: + self.httpapi.login(self.get_option("remote_user"), self.get_option("password")) + + def close(self): + """ + Close the active session to the device + """ + # only close the connection if its connected. + if self._connected: + self.queue_message("vvvv", "closing http(s) connection to device") + self.logout() + + super(Connection, self).close() + + @ensure_connect + def send(self, path, data, retries=None, **kwargs): + """ + Sends the command to the device over api + """ + url_kwargs = dict( + headers={}, + use_proxy=self.get_option("use_proxy"), + timeout=self.get_option("persistent_command_timeout"), + validate_certs=self.get_option("validate_certs"), + http_agent=self.get_option("http_agent"), + client_cert=self.get_option("client_cert"), + client_key=self.get_option("client_key"), + ca_path=self.get_option("ca_path"), + ) + url_kwargs.update(kwargs) + + ciphers = self.get_option("ciphers") + if ciphers: + if Version(ANSIBLE_CORE_VERSION) >= Version("2.14.0"): + # Only insert "ciphers" kwarg for ansible-core versions >= 2.14.0. + url_kwargs["ciphers"] = ciphers + else: + # Emit warning when "ansible_httpapi_ciphers" is set but not supported + self.queue_message( + "warning", + "'ansible_httpapi_ciphers' option is unavailable on ansible-core<2.14", + ) + + if self._auth: + # Avoid modifying passed-in headers + headers = dict(kwargs.get("headers", {})) + headers.update(self._auth) + url_kwargs["headers"] = headers + else: + url_kwargs["force_basic_auth"] = True + url_kwargs["url_username"] = self.get_option("remote_user") + url_kwargs["url_password"] = self.get_option("password") + + try: + url = self._url + path + self._log_messages( + "send url '%s' with data '%s' and kwargs '%s'" % (url, data, url_kwargs) + ) + response = open_url(url, data=data, **url_kwargs) + except HTTPError as exc: + is_handled = self.handle_httperror(exc) + if is_handled is True: + if retries is None: + # The default behavior, retry indefinitely until timeout. + return self.send(path, data, **kwargs) + if retries: + return self.send(path, data, retries=retries - 1, **kwargs) + raise + if is_handled is False: + raise + response = is_handled + except URLError as exc: + raise AnsibleConnectionFailure( + "Could not connect to {0}: {1}".format(self._url + path, exc.reason) + ) + + response_buffer = BytesIO() + resp_data = response.read() + self._log_messages("received response: '%s'" % resp_data) + response_buffer.write(resp_data) + + # Try to assign a new auth token if one is given + self._auth = self.update_auth(response, response_buffer) or self._auth + + response_buffer.seek(0) + + return response, response_buffer + + def transport_test(self, connect_timeout): + """This method enables wait_for_connection to work. + + The sole purpose of this method is to raise an exception if the API's URL + cannot be reached. As such, it does not do anything except attempt to + request the root URL with no error handling. + """ + + open_url(self._url, timeout=connect_timeout) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/connection/libssh.py b/collections/ansible_collections/ansible/netcommon/plugins/connection/libssh.py new file mode 100644 index 0000000..2c8fc86 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/connection/libssh.py @@ -0,0 +1,780 @@ +# (c) 2020 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 = """ + author: + - Ansible Networking Team (@ansible-network) + name: libssh + short_description: Run tasks using libssh for ssh connection + description: + - Use the ansible-pylibssh python bindings to connect to targets + - The python bindings use libssh C library (https://www.libssh.org/) to connect to targets + - This plugin borrows a lot of settings from the ssh plugin as they both cover the same protocol. + version_added: 1.1.0 + options: + remote_addr: + description: + - Address of the remote target + type: string + default: inventory_hostname + vars: + - name: inventory_hostname + - name: ansible_host + - name: ansible_ssh_host + - name: ansible_libssh_host + remote_user: + description: + - User to login/authenticate as + - Can be set from the CLI via the C(--user) or C(-u) options. + type: string + vars: + - name: ansible_user + - name: ansible_ssh_user + - name: ansible_libssh_user + env: + - name: ANSIBLE_REMOTE_USER + - name: ANSIBLE_LIBSSH_REMOTE_USER + ini: + - section: defaults + key: remote_user + - section: libssh_connection + key: remote_user + password: + description: + - Secret used to either login the ssh server or as a passphrase for ssh keys that require it + - Can be set from the CLI via the C(--ask-pass) option. + type: string + vars: + - name: ansible_password + - name: ansible_ssh_pass + - name: ansible_ssh_password + - name: ansible_libssh_pass + - name: ansible_libssh_password + password_prompt: + description: + - Text to match when using keyboard-interactive authentication to determine if the prompt is + for the password. + - Requires ansible-pylibssh version >= 1.0.0 + type: string + vars: + - name: ansible_libssh_password_prompt + version_added: 3.1.0 + private_key_passphrase: + description: + - Passphrase used to unlock the private key specified by the C(ansible_private_key_file) attribute. + - This is required if the private key is encrypted with a passphrase. + type: string + vars: + - name: ansible_private_key_password + - name: ansible_private_key_passphrase + host_key_auto_add: + description: 'TODO: write it' + env: [{name: ANSIBLE_LIBSSH_HOST_KEY_AUTO_ADD}] + ini: + - {key: host_key_auto_add, section: libssh_connection} + type: boolean + look_for_keys: + default: True + description: 'TODO: write it' + env: [{name: ANSIBLE_LIBSSH_LOOK_FOR_KEYS}] + ini: + - {key: look_for_keys, section: libssh_connection} + type: boolean + proxy_command: + default: '' + description: + - Proxy information for running the connection via a jumphost. + - Also this plugin will scan 'ssh_args', 'ssh_extra_args' and 'ssh_common_args' from the 'ssh' plugin settings for proxy information if set. + type: string + env: + - name: ANSIBLE_LIBSSH_PROXY_COMMAND + ini: + - {key: proxy_command, section: libssh_connection} + vars: + - name: ansible_paramiko_proxy_command + - name: ansible_libssh_proxy_command + pty: + default: True + description: 'TODO: write it' + env: + - name: ANSIBLE_LIBSSH_PTY + ini: + - section: libssh_connection + key: pty + type: boolean + publickey_accepted_algorithms: + default: '' + description: + - List of algorithms to forward to SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES. + type: str + env: + - name: ANSIBLE_LIBSSH_PUBLICKEY_ALGORITHMS + ini: + - {key: publickey_algorithms, section: libssh_connection} + vars: + - name: ansible_libssh_publickey_algorithms + hostkeys: + default: '' + description: Set the preferred server host key types as a comma-separated list (e.g., ssh-rsa,ssh-dss,ecdh-sha2-nistp256). + type: str + env: + - name: ANSIBLE_LIBSSH_HOSTKEYS + ini: + - {key: hostkeys, section: libssh_connection} + vars: + - name: ansible_libssh_hostkeys + key_exchange_algorithms: + description: + - Set the key exchange method as a comma-separated list (e.g., "ecdh-sha2-nistp256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1"). + - The list can be prepended by +,-,^ which will append, remove or move to the beginning (prioritizing) of the default list respectively. + Giving an empty list after + and ^ will cause error. + type: str + env: + - name: ANSIBLE_LIBSSH_KEY_EXCHANGE_ALGORITHMS + ini: + - key: key_exchange_algorithms + section: libssh_connection + vars: + - name: ansible_libssh_key_exchange_algorithms + host_key_checking: + description: 'Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host' + type: boolean + default: True + env: + - name: ANSIBLE_HOST_KEY_CHECKING + - name: ANSIBLE_SSH_HOST_KEY_CHECKING + - name: ANSIBLE_LIBSSH_HOST_KEY_CHECKING + ini: + - section: defaults + key: host_key_checking + - section: libssh_connection + key: host_key_checking + vars: + - name: ansible_host_key_checking + - name: ansible_ssh_host_key_checking + - name: ansible_libssh_host_key_checking + use_persistent_connections: + description: 'Toggles the use of persistence for connections' + type: boolean + default: False + env: + - name: ANSIBLE_USE_PERSISTENT_CONNECTIONS + ini: + - section: defaults + key: use_persistent_connections + ssh_args: + version_added: 3.2.0 + description: + - Arguments to pass to all ssh CLI tools. + - ProxyCommand is the only supported argument. + - This option is deprecated in favor of I(proxy_command) and will be removed + in a release after 2026-01-01. + type: string + ini: + - section: 'ssh_connection' + key: 'ssh_args' + env: + - name: ANSIBLE_SSH_ARGS + vars: + - name: ansible_ssh_args + cli: + - name: ssh_args + ssh_common_args: + version_added: 3.2.0 + description: + - Common extra arguments for all ssh CLI tools. + - ProxyCommand is the only supported argument. + - This option is deprecated in favor of I(proxy_command) and will be removed + in a release after 2026-01-01. + type: string + ini: + - section: 'ssh_connection' + key: 'ssh_common_args' + env: + - name: ANSIBLE_SSH_COMMON_ARGS + vars: + - name: ansible_ssh_common_args + cli: + - name: ssh_common_args + ssh_extra_args: + version_added: 3.2.0 + description: + - Extra arguments exclusive to the 'ssh' CLI tool. + - ProxyCommand is the only supported argument. + - This option is deprecated in favor of I(proxy_command) and will be removed + in a release after 2026-01-01. + type: string + vars: + - name: ansible_ssh_extra_args + env: + - name: ANSIBLE_SSH_EXTRA_ARGS + ini: + - key: ssh_extra_args + section: ssh_connection + cli: + - name: ssh_extra_args + config_file: + version_added: 5.1.0 + description: Alternate SSH config file location + type: path + env: + - name: ANSIBLE_LIBSSH_CONFIG_FILE + ini: + - section: libssh_connection + key: config_file + vars: + - name: ansible_libssh_config_file +# TODO: +#timeout=self._play_context.timeout, +""" +import logging +import os +import re +import socket + +from ansible import constants as C +from ansible.errors import AnsibleConnectionFailure, AnsibleError, AnsibleFileNotFound +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text +from ansible.plugins.connection import ConnectionBase +from ansible.utils.display import Display + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.version import Version + + +display = Display() + +try: + from pylibsshext import __version__ as PYLIBSSH_VERSION + from pylibsshext.errors import LibsshSCPException, LibsshSessionException + from pylibsshext.session import Session + + HAS_PYLIBSSH = True +except ImportError: + HAS_PYLIBSSH = False + + +AUTHENTICITY_MSG = """ +libssh: The authenticity of host '%s' can't be established due to '%s'. +The %s key fingerprint is %s. +Are you sure you want to continue connecting (yes/no)? +""" + +# SSH Options Regex +SETTINGS_REGEX = re.compile(r"(\w+)(?:\s*=\s*|\s+)(.+)") + + +class MyAddPolicy(object): + """ + Based on AutoAddPolicy in paramiko so we can determine when keys are added + + and also prompt for input. + + Policy for automatically adding the hostname and new host key to the + local L{HostKeys} object, and saving it. This is used by L{SSHClient}. + """ + + def __init__(self, connection): + self.connection = connection + self._options = connection._options + + def missing_host_key(self, session, hostname, username, key_type, fingerprint, message): + if all( + ( + self._options["host_key_checking"], + not self._options["host_key_auto_add"], + ) + ): + if ( + self.connection.get_option("use_persistent_connections") + or self.connection.force_persistence + ): + # don't print the prompt string since the user cannot respond + # to the question anyway + raise AnsibleError( + AUTHENTICITY_MSG.rsplit("\n", 2)[0] % (hostname, message, key_type, fingerprint) + ) + + inp = to_text( + display.prompt_until( + AUTHENTICITY_MSG % (hostname, message, key_type, fingerprint), private=False + ), + errors="surrogate_or_strict", + ) + + self.connection.connection_unlock() + if inp not in ["yes", "y", ""]: + raise AnsibleError("host connection rejected by user") + + session.hostkey_auto_add(username) + + # host keys are actually saved in close() function below + # in order to control ordering. + + +# keep connection objects on a per host basis to avoid repeated attempts to reconnect +SSH_CONNECTION_CACHE = {} +SFTP_CONNECTION_CACHE = {} + + +class Connection(ConnectionBase): + """SSH based connections with Libssh""" + + transport = "ansible.netcommon.libssh" + _log_channel = None + + def _cache_key(self): + return "%s__%s__" % ( + self._play_context.remote_addr, + self._play_context.remote_user, + ) + + def _connect(self): + cache_key = self._cache_key() + if cache_key in SSH_CONNECTION_CACHE: + self.ssh = SSH_CONNECTION_CACHE[cache_key] + else: + self.ssh = SSH_CONNECTION_CACHE[cache_key] = self._connect_uncached() + return self + + def _set_log_channel(self, name): + self._log_channel = name + + @staticmethod + def _pylibssh_handler_log_level(): + verbosity = display.verbosity + if verbosity >= 3: + return logging.DEBUG + if verbosity >= 1: + return logging.INFO + return logging.WARNING + + def _pylibssh_resolve_log_path(self, host): + logpath = getattr(C, "DEFAULT_LOG_PATH", None) + display.vvvv( + "libssh log handler: DEFAULT_LOG_PATH=%s" % repr(logpath), + host=host, + ) + if not logpath: + display.vvvv("libssh log handler: skipped (no DEFAULT_LOG_PATH set)", host=host) + return None + if not os.path.isabs(logpath): + display.vvvv( + "libssh log handler: skipped (log_path not absolute: %s)" % logpath, + host=host, + ) + return None + logpath = os.path.expanduser(logpath) + if os.path.exists(logpath): + if not os.access(logpath, os.W_OK): + display.vvvv( + "libssh log handler: skipped (log file not writable: %s)" % logpath, + host=host, + ) + return None + return logpath + parent = os.path.dirname(logpath) + if parent and not os.path.isdir(parent): + display.vvvv( + "libssh log handler: skipped (parent not a directory: %s)" % parent, + host=host, + ) + return None + if parent and not os.access(parent, os.W_OK): + display.vvvv( + "libssh log handler: skipped (parent not writable: %s)" % parent, + host=host, + ) + return None + return logpath + + def _pylibssh_attach_log_handler(self, logpath, log_level, host): + pylibssh_log = logging.getLogger("ansible-pylibssh") + for h in pylibssh_log.handlers: + if getattr(h, "baseFilename", None) == logpath: + pylibssh_log.setLevel(log_level) + h.setLevel(log_level) + display.vvvv( + "libssh log handler: already attached to %s (level=%s)" % (logpath, log_level), + host=host, + ) + return + handler = logging.FileHandler(logpath, mode="a", encoding="utf-8") + handler.setLevel(log_level) + handler.setFormatter( + logging.Formatter("%(asctime)s p=%(process)d n=%(name)s %(levelname)s| %(message)s") + ) + pylibssh_log.addHandler(handler) + pylibssh_log.setLevel(log_level) + display.vvvv( + "libssh log handler: added FileHandler for ansible-pylibssh -> %s (level=%s)" + % (logpath, log_level), + host=host, + ) + + def _ensure_pylibssh_log_handler(self, host=None): + """Route ansible-pylibssh (libssh) logs to Ansible log_path when set. + + Handler and logger levels follow display.verbosity using Python standard + logging levels: 0 -> WARNING, 1-2 -> INFO, 3+ -> DEBUG. + """ + log_level = self._pylibssh_handler_log_level() + logpath = self._pylibssh_resolve_log_path(host) + if logpath is None: + return + self._pylibssh_attach_log_handler(logpath, log_level, host) + + def _get_proxy_command(self, port=22): + proxy_command = None + # Parse ansible_ssh_common_args, specifically looking for ProxyCommand + ssh_args = [ + self.get_option("ssh_extra_args") or "", + self.get_option("ssh_common_args") or "", + self.get_option("ssh_args") or "", + ] + + if any(ssh_args): + display.warning( + "The ssh_*_args options are deprecated and will be removed in a release after 2026-01-01. Please use the proxy_command option instead." + ) + args = self._split_ssh_args(" ".join(ssh_args)) + for i, arg in enumerate(args): + if arg.lower() == "proxycommand": + # _split_ssh_args split ProxyCommand from the command itself + proxy_command = args[i + 1] + else: + # ProxyCommand and the command itself are a single string + match = SETTINGS_REGEX.match(arg) + if match: + if match.group(1).lower() == "proxycommand": + proxy_command = match.group(2) + + if proxy_command: + break + + proxy_command = proxy_command or self.get_option("proxy_command") + + if proxy_command: + replacers = { + "%h": self._play_context.remote_addr, + "%p": port, + "%r": self._play_context.remote_user, + } + for find, replace in replacers.items(): + proxy_command = proxy_command.replace(find, str(replace)) + + return proxy_command + + def _connect_uncached(self): + """activates the connection object""" + + if not HAS_PYLIBSSH: + raise AnsibleError(missing_required_lib("ansible-pylibssh")) + display.vvv( + "USING PYLIBSSH VERSION %s" % PYLIBSSH_VERSION, + host=self._play_context.remote_addr, + ) + + ssh_connect_kwargs = {} + + remote_user = self.get_option("remote_user") + remote_addr = self.get_option("remote_addr") + port = self._play_context.port or 22 + display.vvv( + "ESTABLISH LIBSSH CONNECTION FOR USER: %s on PORT %s TO %s" + % (remote_user, port, remote_addr), + host=remote_addr, + ) + + self.ssh = Session() + self._ensure_pylibssh_log_handler(remote_addr) + + self.keyfile = os.path.expanduser("~/.ssh/known_hosts") + + proxy_command = self._get_proxy_command(port) + + try: + private_key = None + if self._play_context.private_key_file: + with open(os.path.expanduser(self._play_context.private_key_file)) as fp: + b_content = fp.read() + private_key = to_bytes(b_content, errors="surrogate_or_strict") + + if proxy_command: + ssh_connect_kwargs["proxycommand"] = proxy_command + + if self.get_option("config_file"): + ssh_connect_kwargs["config_file"] = self.get_option("config_file") + + if self.get_option("password_prompt") and (Version(PYLIBSSH_VERSION) < "1.0.0"): + raise AnsibleError( + "Configuring password prompt is not supported in ansible-pylibssh version %s. " + "Please upgrade to ansible-pylibssh 1.0.0 or newer." % PYLIBSSH_VERSION + ) + + if self.get_option("publickey_accepted_algorithms"): + ssh_connect_kwargs["publickey_accepted_algorithms"] = self.get_option( + "publickey_accepted_algorithms" + ) + + if self.get_option("hostkeys"): + ssh_connect_kwargs["hostkeys"] = self.get_option("hostkeys") + + if self.get_option("key_exchange_algorithms"): + ssh_connect_kwargs["key_exchange_algorithms"] = self.get_option( + "key_exchange_algorithms" + ) + + self.ssh.set_missing_host_key_policy(MyAddPolicy(self)) + + self.ssh.connect( + host=remote_addr.lower(), + user=remote_user, + look_for_keys=self.get_option("look_for_keys"), + host_key_checking=self.get_option("host_key_checking"), + password=self.get_option("password"), + password_prompt=self.get_option("password_prompt"), + private_key=private_key, + private_key_password=self.get_option("private_key_passphrase"), + timeout=self._play_context.timeout, + port=port, + **ssh_connect_kwargs, + ) + except LibsshSessionException as e: + msg = "ssh connection failed: " + to_text(e) + raise AnsibleConnectionFailure(msg) + except Exception as e: + raise AnsibleConnectionFailure(to_text(e)) + + display.vvv("ssh connection is OK: " + str(self.ssh)) + return self.ssh + + def exec_command(self, cmd, in_data=None, sudoable=True): + """run a command on the remote host""" + + super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) + + if in_data: + raise AnsibleError( + "Internal Error: this module does not support optimized module pipelining" + ) + + bufsize = 4096 + + try: + self.chan = self.ssh.new_channel() + except Exception as e: + text_e = to_text(e) + msg = "Failed to open session" + if text_e: + msg += ": %s" % text_e + raise AnsibleConnectionFailure(to_native(msg)) + + # sudo usually requires a PTY (cf. requiretty option), therefore + # we give it one by default (pty=True in ansible.cfg), and we try + # to initialise from the calling environment when sudoable is enabled + if self.get_option("pty") and sudoable: + self.chan.request_shell() + + display.vvv("EXEC %s" % cmd, host=self._play_context.remote_addr) + + cmd = to_bytes(cmd, errors="surrogate_or_strict") + + result = None + no_prompt_out = b"" + no_prompt_err = b"" + become_output = b"" + out = b"" + err = b"" + + try: + if self.become and self.become.expect_prompt(): + passprompt = False + become_sucess = False + self.chan.sendall(cmd) + + while not (become_sucess or passprompt): + display.debug("Waiting for Privilege Escalation input") + self.chan.poll(timeout=self._play_context.timeout) + chunk = self.chan.recv(bufsize) + display.debug("chunk is: %s" % chunk) + + if not chunk: + if b"unknown user" in become_output: + n_become_user = to_native( + self.become.get_option( + "become_user", + playcontext=self._play_context, + ) + ) + raise AnsibleError("user %s does not exist" % n_become_user) + else: + break + # raise AnsibleError('ssh connection closed waiting for password prompt') + become_output += chunk + # need to check every line because we might get lectured + # and we might get the middle of a line in a chunk + for line in become_output.splitlines(True): + if self.become.check_success(line): + become_sucess = True + break + if self.become.check_password_prompt(line): + passprompt = True + break + if passprompt: + if self.become: + become_pass = self.become.get_option( + "become_pass", playcontext=self._play_context + ) + self.chan.sendall( + to_bytes(become_pass, errors="surrogate_or_strict") + b"\n" + ) + else: + raise AnsibleError("A password is required but none was supplied") + else: + no_prompt_out += become_output + no_prompt_err += become_output + else: + result = self.chan.exec_command(to_text(cmd, errors="surrogate_or_strict")) + except socket.timeout: + raise AnsibleError("ssh timed out waiting for privilege escalation.\n" + become_output) + + if result: + rc = result.returncode + out = result.stdout + err = result.stderr + else: + rc = self.chan.get_channel_exit_status() + return rc, out, err + + def put_file(self, in_path, out_path, proto="sftp"): + """transfer a file from local to remote""" + + super(Connection, self).put_file(in_path, out_path) + + display.vvv( + "PUT %s TO %s" % (in_path, out_path), + host=self._play_context.remote_addr, + ) + + if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")): + raise AnsibleFileNotFound("file or module does not exist: %s" % in_path) + + self._connect() + if proto == "sftp": + try: + self.sftp = self.ssh.sftp() + except Exception as e: + raise AnsibleError("failed to open a SFTP connection (%s)" % e) + + try: + self.sftp.put( + to_bytes(in_path, errors="surrogate_or_strict"), + to_bytes(out_path, errors="surrogate_or_strict"), + ) + except IOError: + raise AnsibleError("failed to transfer file to %s" % out_path) + elif proto == "scp": + scp = self.ssh.scp() + try: + scp.put(in_path, out_path) + except LibsshSCPException as exc: + raise AnsibleError("Error transferring file to %s: %s" % (out_path, to_text(exc))) + else: + raise AnsibleError("Don't know how to transfer file over protocol %s" % proto) + + def _connect_sftp(self): + cache_key = "%s__%s__" % ( + self._play_context.remote_addr, + self._play_context.remote_user, + ) + if cache_key in SFTP_CONNECTION_CACHE: + return SFTP_CONNECTION_CACHE[cache_key] + else: + result = SFTP_CONNECTION_CACHE[cache_key] = self._connect().ssh.sftp() + return result + + def _invalidate_ssh_session_after_scp_get_failure(self): + """Drop cached SSH/SFTP and close session after failed SCP get (device quirks).""" + cache_key = self._cache_key() + SSH_CONNECTION_CACHE.pop(cache_key, None) + SFTP_CONNECTION_CACHE.pop(cache_key, None) + if hasattr(self, "ssh") and self.ssh is not None: + try: + self.ssh.close() + except Exception: + pass + self.ssh = None + + def _fetch_file_via_sftp(self, in_path, out_path): + try: + self.sftp = self._connect_sftp() + except Exception as e: + raise AnsibleError("failed to open a SFTP connection (%s)" % to_native(e)) + + try: + self.sftp.get( + to_bytes(in_path, errors="surrogate_or_strict"), + to_bytes(out_path, errors="surrogate_or_strict"), + ) + except IOError: + raise AnsibleError("failed to transfer file from %s" % in_path) + + def _fetch_file_via_scp(self, in_path, out_path): + scp = self.ssh.scp() + try: + # this abruptly closes the connection when + # scp.get fails only when the file is not there + # it works fine if the file is actually present + scp.get(in_path, out_path) + except LibsshSCPException as exc: + # Invalidate cached connection so next operation (e.g. put_file) + # uses a fresh connection; some devices misbehave when reusing + # a connection that had a failed SCP get. + self._invalidate_ssh_session_after_scp_get_failure() + raise AnsibleError("Error transferring file from %s: %s" % (in_path, to_text(exc))) + + def fetch_file(self, in_path, out_path, proto="sftp"): + """save a remote file to the specified path""" + + super(Connection, self).fetch_file(in_path, out_path) + + display.vvv( + "FETCH %s TO %s" % (in_path, out_path), + host=self._play_context.remote_addr, + ) + + if proto == "sftp": + self._fetch_file_via_sftp(in_path, out_path) + elif proto == "scp": + self._fetch_file_via_scp(in_path, out_path) + else: + raise AnsibleError("Don't know how to transfer file over protocol %s" % proto) + + def reset(self): + self.close() + self._connect() + + def close(self): + """terminate the connection""" + + cache_key = self._cache_key() + SSH_CONNECTION_CACHE.pop(cache_key, None) + SFTP_CONNECTION_CACHE.pop(cache_key, None) + + if hasattr(self, "sftp"): + if self.sftp is not None: + self.sftp.close() + + if hasattr(self, "chan"): + if self.chan is not None: + self.chan.close() + + self.ssh.close() + self._connected = False diff --git a/collections/ansible_collections/ansible/netcommon/plugins/connection/netconf.py b/collections/ansible_collections/ansible/netcommon/plugins/connection/netconf.py new file mode 100644 index 0000000..264b91a --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/connection/netconf.py @@ -0,0 +1,470 @@ +# (c) 2016 Red Hat Inc. +# (c) 2017 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 = """ +author: + - Ansible Networking Team (@ansible-network) +name: netconf +short_description: Provides a persistent connection using the netconf protocol +description: +- This connection plugin provides a connection to remote devices over the SSH NETCONF + subsystem. This connection plugin is typically used by network devices for sending + and receiving RPC calls over NETCONF. +- Note this connection plugin requires ncclient to be installed on the local Ansible + controller. +version_added: 1.0.0 +requirements: +- ncclient +extends_documentation_fragment: +- ansible.netcommon.connection_persistent +options: + host: + description: + - Specifies the remote device FQDN or IP address to establish the SSH connection + to. + default: inventory_hostname + type: string + vars: + - name: inventory_hostname + - name: ansible_host + port: + type: int + description: + - Specifies the port on the remote device that listens for connections when establishing + the SSH connection. + default: 830 + ini: + - section: defaults + key: remote_port + env: + - name: ANSIBLE_REMOTE_PORT + vars: + - name: ansible_port + use_libssh: + type: bool + description: + - specifies whether to use libssh for netconf connection or not + default: false + ini: + - section: defaults + key: netconf_libssh + env: + - name: ANSIBLE_NETCONF_LIBSSH + vars: + - name: ansible_netconf_libssh + network_os: + description: + - Configures the device platform network operating system. This value is used + to load a device specific netconf plugin. If this option is not configured + (or set to C(auto)), then Ansible will attempt to guess the correct network_os + to use. If it can not guess a network_os correctly it will use C(default). + type: string + vars: + - name: ansible_network_os + remote_user: + description: + - The username used to authenticate to the remote device when the SSH connection + is first established. If the remote_user is not specified, the connection will + use the username of the logged in user. + - Can be configured from the CLI via the C(--user) or C(-u) options. + type: string + ini: + - section: defaults + key: remote_user + env: + - name: ANSIBLE_REMOTE_USER + vars: + - name: ansible_user + password: + description: + - Configures the user password used to authenticate to the remote device when + first establishing the SSH connection. + type: string + vars: + - name: ansible_password + - name: ansible_ssh_pass + - name: ansible_ssh_password + - name: ansible_netconf_password + private_key_file: + description: + - The private SSH key or certificate file used to authenticate to the remote device + when first establishing the SSH connection. + type: string + ini: + - section: defaults + key: private_key_file + env: + - name: ANSIBLE_PRIVATE_KEY_FILE + vars: + - name: ansible_private_key_file + look_for_keys: + default: true + description: + - Enables looking for ssh keys in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`). + - This option is not supported when C(use_libssh=True),it will be ignored if C(use_libssh) is enabled + env: + - name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS + ini: + - section: paramiko_connection + key: look_for_keys + type: boolean + host_key_checking: + description: Set this to "False" if you want to avoid host key checking by the + underlying tools Ansible uses to connect to the host + type: boolean + default: true + env: + - name: ANSIBLE_HOST_KEY_CHECKING + - name: ANSIBLE_SSH_HOST_KEY_CHECKING + - name: ANSIBLE_NETCONF_HOST_KEY_CHECKING + ini: + - section: defaults + key: host_key_checking + - section: paramiko_connection + key: host_key_checking + vars: + - name: ansible_host_key_checking + - name: ansible_ssh_host_key_checking + - name: ansible_netconf_host_key_checking + proxy_command: + default: '' + description: + - Proxy information for running the connection via a jumphost. + - This requires ncclient >= 0.6.10 to be installed on the controller. + type: string + env: + - name: ANSIBLE_NETCONF_PROXY_COMMAND + ini: + - {key: proxy_command, section: paramiko_connection} + vars: + - name: ansible_paramiko_proxy_command + - name: ansible_netconf_proxy_command + netconf_ssh_config: + description: + - This variable is used to enable bastion/jump host with netconf connection. If + set to True the bastion/jump host ssh settings should be present in ~/.ssh/config + file, alternatively it can be set to custom ssh configuration file path to read + the bastion/jump host settings. + - This option is not supported when C(use_libssh=True), it will be ignored if C(use_libssh) is enabled + type: string + ini: + - section: netconf_connection + key: ssh_config + env: + - name: ANSIBLE_NETCONF_SSH_CONFIG + vars: + - name: ansible_netconf_ssh_config +""" + +import json +import logging +import os + +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text +from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE +from ansible.module_utils.six.moves import cPickle +from ansible.playbook.play_context import PlayContext +from ansible.plugins.connection import ensure_connect +from ansible.plugins.loader import netconf_loader + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.connection_base import ( + NetworkConnectionBase, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.version import Version + + +try: + from ncclient import __version__ as NCCLIENT_VERSION + from ncclient import manager + from ncclient.operations import RPCError + from ncclient.transport.errors import AuthenticationError, SSHUnknownHostError + from ncclient.xml_ import to_ele, to_xml + from paramiko import ProxyCommand + + HAS_NCCLIENT = True + NCCLIENT_IMP_ERR = None +# paramiko and gssapi are incompatible and raise AttributeError not ImportError +# When running in FIPS mode, cryptography raises InternalError +# https://bugzilla.redhat.com/show_bug.cgi?id=1778939 +except Exception as err: + HAS_NCCLIENT = False + NCCLIENT_IMP_ERR = err + +logging.getLogger("ncclient").setLevel(logging.INFO) + + +class Connection(NetworkConnectionBase): + """NetConf connections""" + + transport = "ansible.netcommon.netconf" + has_pipelining = False + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + + # If network_os is not specified then set the network os to auto + # This will be used to trigger the use of guess_network_os when connecting. + self._network_os = self._network_os or "auto" + + self.netconf = netconf_loader.get(self._network_os, self) + if self.netconf: + self._sub_plugin = { + "type": "netconf", + "name": self.netconf._load_name, + "obj": self.netconf, + } + self.queue_message( + "vvvv", + "loaded netconf plugin %s from path %s for network_os %s" + % ( + self.netconf._load_name, + self.netconf._original_path, + self._network_os, + ), + ) + else: + self.netconf = netconf_loader.get("default", self) + self._sub_plugin = { + "type": "netconf", + "name": "default", + "obj": self.netconf, + } + self.queue_message( + "vvvv", + "unable to load netconf plugin for network_os %s, falling back to default plugin" + % self._network_os, + ) + + self.queue_message("log", "network_os is set to %s" % self._network_os) + self._manager = None + self.key_filename = None + self._ssh_config = None + + def exec_command(self, cmd, in_data=None, sudoable=True): + """Sends the request to the node and returns the reply + The method accepts two forms of request. The first form is as a byte + string that represents xml string be send over netconf session. + The second form is a json-rpc (2.0) byte string. + """ + if self._manager: + # to_ele operates on native strings + request = to_ele(to_native(cmd, errors="surrogate_or_strict")) + + if request is None: + return "unable to parse request" + + try: + reply = self._manager.rpc(request) + except RPCError as exc: + error = self.internal_error( + data=to_text(to_xml(exc.xml), errors="surrogate_or_strict") + ) + return json.dumps(error) + + return reply.data_xml + else: + return super(Connection, self).exec_command(cmd, in_data, sudoable) + + def update_play_context(self, pc_data): + """Updates the play context information for the connection""" + pc_data = to_bytes(pc_data) + pc_data = cPickle.loads(pc_data, encoding="bytes") + + play_context = PlayContext() + play_context.deserialize(pc_data) + self._play_context = play_context + + @property + @ensure_connect + def manager(self): + return self._manager + + def _get_proxy_command(self, port=22): + proxy_command = None + + # TO-DO: Add logic to scan ssh_* args to read ProxyCommand + + proxy_command = self.get_option("proxy_command") + + sock = None + if proxy_command: + if Version(NCCLIENT_VERSION) < "0.6.10": + raise AnsibleError( + "Configuring jumphost settings through ProxyCommand is unsupported in ncclient version %s. " + "Please upgrade to ncclient 0.6.10 or newer." % NCCLIENT_VERSION + ) + + replacers = { + "%h": self._play_context.remote_addr, + "%p": port, + "%r": self._play_context.remote_user, + } + + for find, replace in replacers.items(): + proxy_command = proxy_command.replace(find, str(replace)) + sock = ProxyCommand(proxy_command) + + return sock + + def _connect(self): + if not HAS_NCCLIENT: + raise AnsibleError( + "%s: %s" + % ( + missing_required_lib("ncclient"), + to_native(NCCLIENT_IMP_ERR), + ) + ) + + self.queue_message("log", "ssh connection done, starting ncclient") + + allow_agent = True + + if self._play_context.password is not None: + allow_agent = False + setattr(self._play_context, "allow_agent", allow_agent) + + self.key_filename = self._play_context.private_key_file or self.get_option( + "private_key_file" + ) + if self.key_filename: + self.key_filename = str(os.path.expanduser(self.key_filename)) + + self._ssh_config = self.get_option("netconf_ssh_config") + if self._ssh_config in BOOLEANS_TRUE: + self._ssh_config = True + elif self._ssh_config in BOOLEANS_FALSE: + self._ssh_config = None + + # Try to guess the network_os if the network_os is set to auto + if self._network_os == "auto": + for cls in netconf_loader.all(class_only=True): + network_os = cls.guess_network_os(self) + if network_os: + self.queue_message("vvv", "discovered network_os %s" % network_os) + self._network_os = network_os + + # If we have tried to detect the network_os but were unable to i.e. network_os is still 'auto' + # then use default as the network_os + + if self._network_os == "auto": + # Network os not discovered. Set it to default + self.queue_message( + "vvv", + "Unable to discover network_os. Falling back to default.", + ) + self._network_os = "default" + try: + ncclient_device_handler = self.netconf.get_option("ncclient_device_handler") + except KeyError: + ncclient_device_handler = "default" + self.queue_message( + "vvv", + "identified ncclient device handler: %s." % ncclient_device_handler, + ) + device_params = {"name": ncclient_device_handler} + + try: + port = self._play_context.port or 830 + self.queue_message( + "vvv", + "ESTABLISH NETCONF SSH CONNECTION FOR USER: %s on PORT %s TO %s WITH SSH_CONFIG = %s" + % ( + self._play_context.remote_user, + port, + self._play_context.remote_addr, + self._ssh_config, + ), + ) + + use_libssh = self.get_option("use_libssh") + look_for_keys = self.get_option("look_for_keys") + params = dict( + host=self._play_context.remote_addr, + port=port, + username=self._play_context.remote_user, + password=self._play_context.password, + key_filename=self.key_filename, + hostkey_verify=self.get_option("host_key_checking"), + device_params=device_params, + allow_agent=self._play_context.allow_agent, + timeout=self.get_option("persistent_connect_timeout"), + ) + # use_libssh option is only supported with ncclient >= 0.7.0 + if use_libssh: + self.queue_message( + "vvv", + "ncclient is using LIBSSH as transport for this netconf connection ", + ) + if self._ssh_config: + self.queue_message( + "warning", + "ncclient >= 0.7.0 does not support ssh_config file option while using libssh as a transport", + ) + if look_for_keys: + self.queue_message( + "warning", + "ncclient >= 0.7.0 does not support look_for_keys option while using libssh as a transport", + ) + + params["use_libssh"] = use_libssh + + else: + params["look_for_keys"] = look_for_keys + params["ssh_config"] = self._ssh_config + + # sock is only supported by ncclient >= 0.6.10, and will error if + # included on older versions. We check the version in + # _get_proxy_command, so if this returns a value, the version is + # fine and we have something to send. Otherwise, don't even send + # the option to support older versions of ncclient + sock = self._get_proxy_command(port) + if sock: + params["sock"] = sock + + self._manager = manager.connect(**params) + + self._manager._timeout = self.get_option("persistent_command_timeout") + except SSHUnknownHostError as exc: + raise AnsibleConnectionFailure(to_native(exc)) + except AuthenticationError as exc: + if str(exc).startswith("FileNotFoundError"): + raise AnsibleError( + "Encountered FileNotFoundError in ncclient connect. Does {0} exist?".format( + self.key_filename + ) + ) + raise + except ImportError: + raise AnsibleError( + "connection=netconf is not supported on {0}".format(self._network_os) + ) + + if not self._manager.connected: + return 1, b"", b"not connected" + + self.queue_message("log", "ncclient manager object created successfully") + + self._connected = True + + super(Connection, self)._connect() + + return ( + 0, + to_bytes(self._manager.session_id, errors="surrogate_or_strict"), + b"", + ) + + def close(self): + if self._manager and self._manager.connected: + self._manager.close_session() + super(Connection, self).close() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py b/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py new file mode 100644 index 0000000..2a34be5 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/connection/network_cli.py @@ -0,0 +1,1870 @@ +# (c) 2016 Red Hat Inc. +# (c) 2017 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 = """ +author: + - Ansible Networking Team (@ansible-network) +name: network_cli +short_description: Use network_cli to run command on network appliances +description: +- This connection plugin provides a connection to remote devices over the SSH and + implements a CLI shell. This connection plugin is typically used by network devices + for sending and receiving CLi commands to network devices. +version_added: 1.0.0 +requirements: +- ansible-pylibssh if using I(ssh_type=libssh) +extends_documentation_fragment: +- ansible.netcommon.connection_persistent +options: + host: + description: + - Specifies the remote device FQDN or IP address to establish the SSH connection + to. + default: inventory_hostname + type: string + vars: + - name: inventory_hostname + - name: ansible_host + port: + type: int + description: + - Specifies the port on the remote device that listens for connections when establishing + the SSH connection. + default: 22 + ini: + - section: defaults + key: remote_port + env: + - name: ANSIBLE_REMOTE_PORT + vars: + - name: ansible_port + network_os: + description: + - Configures the device platform network operating system. This value is used + to load the correct terminal and cliconf plugins to communicate with the remote + device. + type: string + vars: + - name: ansible_network_os + remote_user: + description: + - The username used to authenticate to the remote device when the SSH connection + is first established. If the remote_user is not specified, the connection will + use the username of the logged in user. + - Can be configured from the CLI via the C(--user) or C(-u) options. + type: string + ini: + - section: defaults + key: remote_user + env: + - name: ANSIBLE_REMOTE_USER + vars: + - name: ansible_user + password: + description: + - Configures the user password used to authenticate to the remote device when + first establishing the SSH connection. + type: string + vars: + - name: ansible_password + - name: ansible_ssh_pass + - name: ansible_ssh_password + private_key_file: + description: + - The private SSH key or certificate file used to authenticate to the remote device + when first establishing the SSH connection. + type: string + ini: + - section: defaults + key: private_key_file + env: + - name: ANSIBLE_PRIVATE_KEY_FILE + vars: + - name: ansible_private_key_file + become: + type: boolean + description: + - The become option will instruct the CLI session to attempt privilege escalation + on platforms that support it. Normally this means transitioning from user mode + to C(enable) mode in the CLI session. If become is set to True and the remote + device does not support privilege escalation or the privilege has already been + elevated, then this option is silently ignored. + - Can be configured from the CLI via the C(--become) or C(-b) options. + default: false + ini: + - section: privilege_escalation + key: become + env: + - name: ANSIBLE_BECOME + vars: + - name: ansible_become + become_errors: + type: str + description: + - This option determines how privilege escalation failures are handled when + I(become) is enabled. + - When set to C(ignore), the errors are silently ignored. + When set to C(warn), a warning message is displayed. + The default option C(fail), triggers a failure and halts execution. + vars: + - name: ansible_network_become_errors + default: fail + choices: ["ignore", "warn", "fail"] + terminal_errors: + type: str + description: + - This option determines how failures while setting terminal parameters + are handled. + - When set to C(ignore), the errors are silently ignored. + When set to C(warn), a warning message is displayed. + The default option C(fail), triggers a failure and halts execution. + vars: + - name: ansible_network_terminal_errors + default: fail + choices: ["ignore", "warn", "fail"] + version_added: 3.1.0 + become_method: + description: + - This option allows the become method to be specified in for handling privilege + escalation. Typically the become_method value is set to C(enable) but could + be defined as other values. + default: sudo + type: string + ini: + - section: privilege_escalation + key: become_method + env: + - name: ANSIBLE_BECOME_METHOD + vars: + - name: ansible_become_method + host_key_auto_add: + type: boolean + description: + - When C(true), unknown host keys are automatically added to the user known_hosts + file and saved on connection close, so the first connection to a new host works + without manual intervention (e.g. no need to run ssh-keyscan or set config). + - When C(false), Ansible would prompt before adding; since network_cli uses + persistent connections (background processes), the user cannot respond, so + connection fails for new hosts unless the key is already in known_hosts. + - Set to C(false) if you need to avoid automatically trusting new host keys + (e.g. on production systems where you manage known_hosts separately). + default: true + ini: + - section: paramiko_connection + key: host_key_auto_add + env: + - name: ANSIBLE_HOST_KEY_AUTO_ADD + persistent_buffer_read_timeout: + type: float + description: + - Configures, in seconds, the amount of time to wait for the data to be read from + Paramiko channel after the command prompt is matched. This timeout value ensures + that command prompt matched is correct and there is no more data left to be + received from remote host. + default: 0.1 + ini: + - section: persistent_connection + key: buffer_read_timeout + env: + - name: ANSIBLE_PERSISTENT_BUFFER_READ_TIMEOUT + vars: + - name: ansible_buffer_read_timeout + terminal_stdout_re: + type: list + elements: dict + description: + - A single regex pattern or a sequence of patterns along with optional flags to + match the command prompt from the received response chunk. This option accepts + C(pattern) and C(flags) keys. The value of C(pattern) is a python regex pattern + to match the response and the value of C(flags) is the value accepted by I(flags) + argument of I(re.compile) python method to control the way regex is matched + with the response, for example I('re.I'). + vars: + - name: ansible_terminal_stdout_re + terminal_stderr_re: + type: list + elements: dict + description: + - This option provides the regex pattern and optional flags to match the error + string from the received response chunk. This option accepts C(pattern) and + C(flags) keys. The value of C(pattern) is a python regex pattern to match the + response and the value of C(flags) is the value accepted by I(flags) argument + of I(re.compile) python method to control the way regex is matched with the + response, for example I('re.I'). + vars: + - name: ansible_terminal_stderr_re + terminal_initial_prompt: + type: list + elements: string + description: + - A single regex pattern or a sequence of patterns to evaluate the expected prompt + at the time of initial login to the remote host. + vars: + - name: ansible_terminal_initial_prompt + terminal_initial_answer: + type: list + elements: string + description: + - The answer to reply with if the C(terminal_initial_prompt) is matched. The value + can be a single answer or a list of answers for multiple terminal_initial_prompt. + In case the login menu has multiple prompts the sequence of the prompt and excepted + answer should be in same order and the value of I(terminal_prompt_checkall) + should be set to I(True) if all the values in C(terminal_initial_prompt) are + expected to be matched and set to I(False) if any one login prompt is to be + matched. + vars: + - name: ansible_terminal_initial_answer + terminal_initial_prompt_checkall: + type: boolean + description: + - By default the value is set to I(False) and any one of the prompts mentioned + in C(terminal_initial_prompt) option is matched it won't check for other prompts. + When set to I(True) it will check for all the prompts mentioned in C(terminal_initial_prompt) + option in the given order and all the prompts should be received from remote + host if not it will result in timeout. + default: false + vars: + - name: ansible_terminal_initial_prompt_checkall + terminal_inital_prompt_newline: + type: boolean + description: + - This boolean flag, that when set to I(True) will send newline in the response + if any of values in I(terminal_initial_prompt) is matched. + default: true + vars: + - name: ansible_terminal_initial_prompt_newline + network_cli_retries: + description: + - Number of attempts to connect to remote host. The delay time between the retires + increases after every attempt by power of 2 in seconds till either the maximum + attempts are exhausted or any of the C(persistent_command_timeout) or C(persistent_connect_timeout) + timers are triggered. + default: 3 + type: integer + env: + - name: ANSIBLE_NETWORK_CLI_RETRIES + ini: + - section: persistent_connection + key: network_cli_retries + vars: + - name: ansible_network_cli_retries + ssh_type: + description: + - The python package that will be used by the C(network_cli) connection plugin to create a SSH connection to remote host. + - I(libssh) will use the ansible-pylibssh package, which needs to be installed in order to work. + - I(paramiko) will instead use the paramiko package to manage the SSH connection. + This option is deprecated and will be removed in a release after 2028-02-01. + Migrate to I(libssh) by installing the ansible-pylibssh package. + - I(auto) will use ansible-pylibssh if that package is installed, otherwise will fallback to paramiko. + default: auto + choices: ["libssh", "paramiko", "auto"] + type: string + env: + - name: ANSIBLE_NETWORK_CLI_SSH_TYPE + ini: + - section: persistent_connection + key: ssh_type + vars: + - name: ansible_network_cli_ssh_type + host_key_checking: + description: 'Set this to "False" if you want to avoid host key checking by the underlying tools Ansible uses to connect to the host' + type: boolean + default: True + env: + - name: ANSIBLE_HOST_KEY_CHECKING + - name: ANSIBLE_SSH_HOST_KEY_CHECKING + ini: + - section: defaults + key: host_key_checking + - section: persistent_connection + key: host_key_checking + vars: + - name: ansible_host_key_checking + - name: ansible_ssh_host_key_checking + single_user_mode: + type: boolean + default: false + version_added: 2.0.0 + description: + - This option enables caching of data fetched from the target for re-use. + The cache is invalidated when the target device enters configuration mode. + - Applicable only for platforms where this has been implemented. + env: + - name: ANSIBLE_NETWORK_SINGLE_USER_MODE + vars: + - name: ansible_network_single_user_mode +""" + +import fcntl +import getpass +import json +import logging +import os +import re +import signal +import socket +import tempfile +import time +import traceback + +from binascii import hexlify +from functools import wraps +from io import BytesIO + +from ansible.errors import AnsibleAuthenticationFailure, AnsibleConnectionFailure, AnsibleError +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text +from ansible.module_utils.six.moves import cPickle +from ansible.playbook.play_context import PlayContext +from ansible.plugins.loader import cache_loader, cliconf_loader, connection_loader, terminal_loader +from ansible.utils.display import Display +from ansible.utils.path import makedirs_safe + +from ansible_collections.ansible.netcommon.plugins.connection.libssh import HAS_PYLIBSSH +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list +from ansible_collections.ansible.netcommon.plugins.plugin_utils.connection_base import ( + NetworkConnectionBase, +) + + +try: + from scp import SCPClient + + HAS_SCP = True +except ImportError: + HAS_SCP = False + +# Import paramiko directly (network_cli's paramiko support is independent of ansible-core) +try: + import paramiko + + from paramiko.ssh_exception import AuthenticationException, BadHostKeyException + + HAS_PARAMIKO = True + PARAMIKO_IMPORT_ERR = None +except ImportError as err: + HAS_PARAMIKO = False + PARAMIKO_IMPORT_ERR = err + paramiko = None + AuthenticationException = None + BadHostKeyException = None + +# Import LooseVersion for paramiko version checks +try: + from ansible.module_utils.compat.version import LooseVersion +except ImportError: + # Fallback for older ansible versions + from distutils.version import LooseVersion + +display = Display() + + +# Paramiko host key authenticity message (prompt for user) +AUTHENTICITY_MSG = """ +paramiko: The authenticity of host '%s' can't be established. +The %s key fingerprint is %s. +Are you sure you want to continue connecting (yes/no)? +""" +# One-line message when we cannot prompt (e.g. persistent connection) +AUTHENTICITY_ERR_MSG = ( + "paramiko: The authenticity of host '%s' can't be established. The %s key fingerprint is %s. " + "Set host_key_auto_add=True to add the key and save it to known_hosts, or add the key manually." +) + +# SSH Options Regex for parsing proxy commands +SETTINGS_REGEX = re.compile(r"(\w+)(?:\s*=\s*|\s+)(.+)") + +# Base class for MissingHostKeyPolicy (paramiko may not be installed) +_MissingHostKeyPolicy = object +if paramiko: + _MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy + + +class _ParamikoHostKeyPolicy(_MissingHostKeyPolicy): + """ + Host key policy for paramiko connections used by network_cli. + + Based on AutoAddPolicy in paramiko so we can determine when keys are added + and also prompt for input. + + Policy for automatically adding the hostname and new host key to the + local HostKeys object, and saving it. + """ + + def __init__(self, connection): + self.connection = connection + + def missing_host_key(self, client, hostname, key): + if all( + ( + self.connection.get_option("host_key_checking"), + not self.connection.get_option("host_key_auto_add"), + ) + ): + fingerprint = to_text(hexlify(key.get_fingerprint()), errors="surrogate_or_strict") + ktype = key.get_name() + + if ( + self.connection.get_option("use_persistent_connections") + or self.connection.force_persistence + ): + # don't print the prompt string since the user cannot respond + # to the question anyway + raise AnsibleError(AUTHENTICITY_ERR_MSG % (hostname, ktype, fingerprint)) + + inp = to_text( + display.prompt_until( + AUTHENTICITY_MSG % (hostname, ktype, fingerprint), private=False + ), + errors="surrogate_or_strict", + ) + + if inp not in ["yes", "y", ""]: + raise AnsibleError("host connection rejected by user") + + key._added_by_ansible_this_time = True + + # existing implementation below: + client._host_keys.add(hostname, key.get_name(), key) + + # host keys are actually saved in close() function below + # in order to control ordering. + + +# Keep paramiko connection objects on a per host basis to avoid repeated attempts to reconnect +_PARAMIKO_SSH_CONNECTION_CACHE = {} +_PARAMIKO_SFTP_CONNECTION_CACHE = {} + + +class _ParamikoConnection: + """ + Internal paramiko-based SSH connection class for network_cli. + + This class provides the paramiko SSH connection functionality that was + previously loaded from ansible-core's paramiko connection plugin. + It implements only the methods and attributes required by network_cli. + + Note: This class intentionally does NOT inherit from ConnectionBase to avoid + issues with Ansible's plugin option system. Options are managed internally. + + .. deprecated:: 7.0.0 + paramiko support was removed from ansible-core in version 2.21. + This compatibility layer is provided as a transitional measure. + Please migrate to using ``ssh_type=libssh`` by installing ansible-pylibssh. + This feature will be removed in a release after 2028-02-01. + """ + + transport = "network_cli_paramiko" + _log_channel = None + + # Default option values for paramiko connection + _PARAMIKO_DEFAULTS = { + "remote_addr": None, + "port": 22, + "remote_user": None, + "password": None, + "private_key_file": None, + "host_key_auto_add": True, + "look_for_keys": True, + "proxy_command": "", + "pty": True, + "record_host_keys": True, + "host_key_checking": True, + "use_rsa_sha2_algorithms": True, + "banner_timeout": 30, + "paramiko_timeout": 10, + "use_persistent_connections": False, + } + + def __init__(self, play_context, new_stdin, parent_connection=None): + self._play_context = play_context + self.ssh = None + self._connected = False + self.keyfile = os.path.expanduser("~/.ssh/known_hosts") + # Store reference to parent connection for option lookups + self._parent_connection = parent_connection + # Attribute used by host key policy + self.force_persistence = False + # Plugin-related attributes expected by Ansible + self._load_name = "network_cli_paramiko" + self._original_path = __file__ + # Initialize options with defaults + self._paramiko_options = dict(self._PARAMIKO_DEFAULTS) + # Initialize from play_context where available + if play_context: + if hasattr(play_context, "remote_addr") and play_context.remote_addr: + self._paramiko_options["remote_addr"] = play_context.remote_addr + if hasattr(play_context, "remote_user") and play_context.remote_user: + self._paramiko_options["remote_user"] = play_context.remote_user + if hasattr(play_context, "port") and play_context.port: + self._paramiko_options["port"] = play_context.port + if hasattr(play_context, "password") and play_context.password: + self._paramiko_options["password"] = play_context.password + if hasattr(play_context, "private_key_file") and play_context.private_key_file: + self._paramiko_options["private_key_file"] = play_context.private_key_file + if hasattr(play_context, "timeout") and play_context.timeout: + self._paramiko_options["paramiko_timeout"] = play_context.timeout + + def get_option(self, option, hostvars=None): + """Get an option value from internal options, parent connection, or defaults. + + Priority order: + 1. Explicitly set options in _paramiko_options (via set_option/set_options) + 2. Parent connection's options (only for options we know about) + 3. Default values in _PARAMIKO_DEFAULTS + + Raises KeyError for unknown options to match expected plugin behavior. + """ + # Only handle options that are relevant to paramiko + # This ensures options like 'ssh_type' that are specific to network_cli + # are not exposed through this connection + if option not in self._PARAMIKO_DEFAULTS: + raise KeyError(option) + + # First check if option was explicitly set on this connection + if option in self._paramiko_options: + return self._paramiko_options[option] + + # Then try to get from parent connection (network_cli) for shared options + if self._parent_connection is not None: + try: + value = self._parent_connection.get_option(option) + if value is not None: + return value + except (KeyError, AttributeError): + pass + + # Return default value + return self._PARAMIKO_DEFAULTS[option] + + def set_option(self, option, value): + """Set an option value.""" + # Store in our internal options + self._paramiko_options[option] = value + + def set_options(self, task_keys=None, var_options=None, direct=None): + """Set multiple options at once. + + This method is called by Ansible to configure the connection plugin. + We only store options that are relevant to paramiko (i.e., in _PARAMIKO_DEFAULTS). + Options like ssh_type that are specific to network_cli are not stored here. + """ + # Only store options that are relevant to paramiko + if direct: + for key, value in direct.items(): + if key in self._PARAMIKO_DEFAULTS: + self._paramiko_options[key] = value + + def get_options(self, hostvars=None): + """Get all options as a dictionary.""" + # Start with defaults + options = dict(self._PARAMIKO_DEFAULTS) + # Override with our stored options + options.update(self._paramiko_options) + return options + + def _cache_key(self): + return "%s__%s__" % (self.get_option("remote_addr"), self.get_option("remote_user")) + + def _set_log_channel(self, name): + """Mimic paramiko.SSHClient.set_log_channel""" + self._log_channel = name + + def _parse_proxy_command(self, port=22): + proxy_command = self.get_option("proxy_command") or None + + sock_kwarg = {} + if proxy_command: + replacers = { + "%h": self.get_option("remote_addr"), + "%p": port, + "%r": self.get_option("remote_user"), + } + for find, replace in replacers.items(): + proxy_command = proxy_command.replace(find, str(replace)) + try: + sock_kwarg = {"sock": paramiko.ProxyCommand(proxy_command)} + display.vvv( + "CONFIGURE PROXY COMMAND FOR CONNECTION: %s" % proxy_command, + host=self.get_option("remote_addr"), + ) + except AttributeError: + display.warning( + "Paramiko ProxyCommand support unavailable. " + "Please upgrade to Paramiko 1.9.0 or newer. " + "Not using configured ProxyCommand" + ) + + return sock_kwarg + + def _connect(self): + cache_key = self._cache_key() + if cache_key in _PARAMIKO_SSH_CONNECTION_CACHE: + self.ssh = _PARAMIKO_SSH_CONNECTION_CACHE[cache_key] + else: + self.ssh = _PARAMIKO_SSH_CONNECTION_CACHE[cache_key] = self._connect_uncached() + + self._connected = True + return self + + def _connect_uncached(self): + """Activates the connection object""" + + if not HAS_PARAMIKO: + raise AnsibleError("paramiko is not installed: %s" % to_native(PARAMIKO_IMPORT_ERR)) + + # Emit deprecation warning for paramiko usage + display.deprecated( + "The paramiko-based SSH transport for network_cli is deprecated. " + "paramiko support is removed from ansible-core in version 2.21 and " + "this compatibility layer in ansible.netcommon is provided as a " + "transitional measure. Please migrate to using ssh_type=libssh by " + "installing ansible-pylibssh. ", + date="2028-02-01", + collection_name="ansible.netcommon", + ) + + # Log paramiko availability for debugging + display.vvvv( + "PARAMIKO: direct import (version %s)" % paramiko.__version__, + host=self.get_option("remote_addr"), + ) + + port = self.get_option("port") or 22 + display.vvv( + "ESTABLISH PARAMIKO SSH CONNECTION FOR USER: %s on PORT %s TO %s" + % (self.get_option("remote_user"), port, self.get_option("remote_addr")), + host=self.get_option("remote_addr"), + ) + + ssh = paramiko.SSHClient() + + # Set pubkey and hostkey algorithms to disable + paramiko_preferred_pubkeys = getattr(paramiko.Transport, "_preferred_pubkeys", ()) + paramiko_preferred_hostkeys = getattr(paramiko.Transport, "_preferred_keys", ()) + use_rsa_sha2_algorithms = self.get_option("use_rsa_sha2_algorithms") + disabled_algorithms = {} + if not use_rsa_sha2_algorithms: + if paramiko_preferred_pubkeys: + disabled_algorithms["pubkeys"] = tuple( + a for a in paramiko_preferred_pubkeys if "rsa-sha2" in a + ) + if paramiko_preferred_hostkeys: + disabled_algorithms["keys"] = tuple( + a for a in paramiko_preferred_hostkeys if "rsa-sha2" in a + ) + + # override paramiko's default logger name + if self._log_channel is not None: + ssh.set_log_channel(self._log_channel) + + self.keyfile = os.path.expanduser("~/.ssh/known_hosts") + + if self.get_option("host_key_checking"): + for ssh_known_hosts in ("/etc/ssh/ssh_known_hosts", "/etc/openssh/ssh_known_hosts"): + try: + ssh.load_system_host_keys(ssh_known_hosts) + break + except OSError: + pass # file was not found, but not required to function + # Load user's known_hosts (same file we write to on close) + if os.path.exists(self.keyfile): + try: + ssh.load_system_host_keys(self.keyfile) + except OSError: + pass + ssh.load_system_host_keys() + + ssh_connect_kwargs = self._parse_proxy_command(port) + + ssh.set_missing_host_key_policy(_ParamikoHostKeyPolicy(self)) + + conn_password = self.get_option("password") + + allow_agent = True + if conn_password is not None: + allow_agent = False + + try: + key_filename = None + if self.get_option("private_key_file"): + key_filename = os.path.expanduser(self.get_option("private_key_file")) + + timeout = self.get_option("paramiko_timeout") or 10 + + # paramiko 2.2 introduced auth_timeout parameter + if LooseVersion(paramiko.__version__) >= LooseVersion("2.2.0"): + ssh_connect_kwargs["auth_timeout"] = timeout + + # paramiko 1.15 introduced banner timeout parameter + if LooseVersion(paramiko.__version__) >= LooseVersion("1.15.0"): + ssh_connect_kwargs["banner_timeout"] = self.get_option("banner_timeout") or 30 + + ssh.connect( + self.get_option("remote_addr").lower(), + username=self.get_option("remote_user"), + allow_agent=allow_agent, + look_for_keys=self.get_option("look_for_keys"), + key_filename=key_filename, + password=conn_password, + timeout=timeout, + port=port, + disabled_algorithms=disabled_algorithms, + **ssh_connect_kwargs, + ) + except BadHostKeyException as e: + raise AnsibleConnectionFailure("host key mismatch for %s" % e.hostname) + except AuthenticationException as e: + msg = "Failed to authenticate: {0}".format(to_text(e)) + raise AnsibleAuthenticationFailure(msg) + except Exception as e: + msg = to_text(e) + if "PID check failed" in msg: + raise AnsibleError( + "paramiko version issue, please upgrade paramiko on the machine running ansible" + ) + elif "Private key file is encrypted" in msg: + msg = "ssh %s@%s:%s : %s\nTo connect as a different user, use -u ." % ( + self.get_option("remote_user"), + self.get_option("remote_addr"), + port, + msg, + ) + raise AnsibleConnectionFailure(msg) + else: + raise AnsibleConnectionFailure(msg) + + return ssh + + def exec_command(self, cmd, in_data=None, sudoable=True): + """Run a command on the remote host (not typically used by network_cli)""" + raise NotImplementedError( + "exec_command is not implemented for network_cli's internal paramiko connection" + ) + + def put_file(self, in_path, out_path): + """Transfer a file from local to remote""" + display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.get_option("remote_addr")) + + if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")): + raise AnsibleError("file or module does not exist: %s" % in_path) + + try: + self.sftp = self.ssh.open_sftp() + except Exception as e: + raise AnsibleError("failed to open a SFTP connection (%s)" % e) + + try: + self.sftp.put( + to_bytes(in_path, errors="surrogate_or_strict"), + to_bytes(out_path, errors="surrogate_or_strict"), + ) + except IOError: + raise AnsibleError("failed to transfer file to %s" % out_path) + + def _connect_sftp(self): + cache_key = "%s__%s__" % (self.get_option("remote_addr"), self.get_option("remote_user")) + if cache_key in _PARAMIKO_SFTP_CONNECTION_CACHE: + return _PARAMIKO_SFTP_CONNECTION_CACHE[cache_key] + else: + result = _PARAMIKO_SFTP_CONNECTION_CACHE[cache_key] = self._connect().ssh.open_sftp() + return result + + def fetch_file(self, in_path, out_path): + """Save a remote file to the specified path""" + display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.get_option("remote_addr")) + + try: + self.sftp = self._connect_sftp() + except Exception as e: + raise AnsibleError("failed to open a SFTP connection (%s)" % to_native(e)) + + try: + self.sftp.get( + to_bytes(in_path, errors="surrogate_or_strict"), + to_bytes(out_path, errors="surrogate_or_strict"), + ) + except IOError: + raise AnsibleError("failed to transfer file from %s" % in_path) + + def _any_keys_added(self): + if self.ssh is None: + return False + for hostname, keys in self.ssh._host_keys.items(): + for keytype, key in keys.items(): + added_this_time = getattr(key, "_added_by_ansible_this_time", False) + if added_this_time: + return True + return False + + def _save_ssh_host_keys(self, filename): + """ + Save SSH host keys - not using the paramiko save_ssh_host_keys function + as we want to add new SSH keys at the bottom. + """ + if not self._any_keys_added(): + return + + path = os.path.expanduser("~/.ssh") + makedirs_safe(path) + + with open(filename, "w") as f: + for hostname, keys in self.ssh._host_keys.items(): + for keytype, key in keys.items(): + added_this_time = getattr(key, "_added_by_ansible_this_time", False) + if not added_this_time: + f.write("%s %s %s\n" % (hostname, keytype, key.get_base64())) + + for hostname, keys in self.ssh._host_keys.items(): + for keytype, key in keys.items(): + added_this_time = getattr(key, "_added_by_ansible_this_time", False) + if added_this_time: + f.write("%s %s %s\n" % (hostname, keytype, key.get_base64())) + + def reset(self): + if not self._connected: + return + self.close() + self._connect() + + def close(self): + """Terminate the connection""" + cache_key = self._cache_key() + _PARAMIKO_SSH_CONNECTION_CACHE.pop(cache_key, None) + _PARAMIKO_SFTP_CONNECTION_CACHE.pop(cache_key, None) + + if hasattr(self, "sftp"): + if self.sftp is not None: + self.sftp.close() + + if ( + self.get_option("host_key_checking") + and self.get_option("record_host_keys") + and self._any_keys_added() + ): + # add any new SSH host keys + lockfile = self.keyfile.replace("known_hosts", ".known_hosts.lock") + dirname = os.path.dirname(self.keyfile) + makedirs_safe(dirname) + + key_lock = open(lockfile, "w") + try: + fcntl.lockf(key_lock, fcntl.LOCK_EX) + + # just in case any were added recently + self.ssh.load_system_host_keys() + self.ssh._host_keys.update(self.ssh._system_host_keys) + + # gather information about the current key file + key_dir = os.path.dirname(self.keyfile) + if os.path.exists(self.keyfile): + key_stat = os.stat(self.keyfile) + mode = key_stat.st_mode + uid = key_stat.st_uid + gid = key_stat.st_gid + else: + mode = 33188 + uid = os.getuid() + gid = os.getgid() + + # Save the new keys to a temporary file and move it into place + tmp_keyfile = tempfile.NamedTemporaryFile(dir=key_dir, delete=False) + os.chmod(tmp_keyfile.name, mode & 0o7777) + if hasattr(os, "chown"): + os.chown(tmp_keyfile.name, uid, gid) + + self._save_ssh_host_keys(tmp_keyfile.name) + tmp_keyfile.close() + + os.rename(tmp_keyfile.name, self.keyfile) + + except Exception: + # unable to save keys, including scenario when key was invalid + traceback.print_exc() + finally: + fcntl.lockf(key_lock, fcntl.LOCK_UN) + key_lock.close() + + if self.ssh: + self.ssh.close() + self._connected = False + + +def ensure_connect(func): + @wraps(func) + def wrapped(self, *args, **kwargs): + if not self._connected: + self._connect() + self.update_cli_prompt_context() + return func(self, *args, **kwargs) + + return wrapped + + +class AnsibleCmdRespRecv(Exception): + pass + + +class Connection(NetworkConnectionBase): + """CLI (shell) SSH connections on Paramiko""" + + transport = "ansible.netcommon.network_cli" + has_pipelining = True + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + self._ssh_shell = None + + self._matched_prompt = None + self._matched_cmd_prompt = None + self._matched_pattern = None + self._last_response = None + self._history = list() + self._command_response = None + self._last_recv_window = None + self._cache = None + + self._terminal = None + self.cliconf = None + + # Managing prompt context + self._check_prompt = False + + self._task_uuid = to_text(kwargs.get("task_uuid", "")) + self._ssh_type_conn = None + self._ssh_type = None + + self._single_user_mode = False + + if self._network_os: + self._terminal = terminal_loader.get(self._network_os, self) + if not self._terminal: + raise AnsibleConnectionFailure("network os %s is not supported" % self._network_os) + + self.cliconf = cliconf_loader.get(self._network_os, self) + if not self.cliconf: + self.queue_message( + "vvvv", + "unable to load cliconf for network_os %s. Falling back to default" + % self._network_os, + ) + self.cliconf = cliconf_loader.get("ansible.netcommon.default", self) + if not self.cliconf: + raise AnsibleConnectionFailure("Couldn't load fallback cliconf plugin") + + self._sub_plugin = { + "type": "cliconf", + "name": self.cliconf._load_name, + "obj": self.cliconf, + } + self.queue_message( + "vvvv", + "loaded cliconf plugin %s from path %s for network_os %s" + % ( + self.cliconf._load_name, + self.cliconf._original_path, + self._network_os, + ), + ) + else: + raise AnsibleConnectionFailure( + "Unable to automatically determine host network os. Please " + "manually configure ansible_network_os value for this host" + ) + self.queue_message("log", "network_os is set to %s" % self._network_os) + + @property + def ssh_type(self): + if self._ssh_type is None: + self._ssh_type = self.get_option("ssh_type") + self.queue_message("vvvv", "ssh type is set to %s" % self._ssh_type) + # Support autodetection of supported library + if self._ssh_type == "auto": + self.queue_message("vvvv", "autodetecting ssh_type") + if HAS_PYLIBSSH: + self._ssh_type = "libssh" + else: + self.queue_message( + "warning", + "ansible-pylibssh not installed, falling back to paramiko", + ) + self._ssh_type = "paramiko" + self.queue_message("vvvv", "ssh type is now set to %s" % self._ssh_type) + + if self._ssh_type not in ["paramiko", "libssh"]: + raise AnsibleConnectionFailure( + "Invalid value '%s' set for ssh_type option." + " Expected value is either 'libssh' or 'paramiko'" % self._ssh_type + ) + + return self._ssh_type + + @property + def ssh_type_conn(self): + if self._ssh_type_conn is None: + if self.ssh_type == "libssh": + # Use the libssh connection plugin from ansible.netcommon + self._ssh_type_conn = connection_loader.get( + "ansible.netcommon.libssh", self._play_context, "/dev/null" + ) + elif self.ssh_type == "paramiko": + # Use the internal paramiko connection implementation + # This removes the dependency on ansible-core's paramiko plugin + self._ssh_type_conn = _ParamikoConnection( + self._play_context, "/dev/null", parent_connection=self + ) + else: + raise AnsibleConnectionFailure( + "Invalid value '%s' set for ssh_type option." + " Expected value is either 'libssh' or 'paramiko'" % self._ssh_type + ) + + return self._ssh_type_conn + + # To maintain backward compatibility + @property + def paramiko_conn(self): + return self.ssh_type_conn + + def _get_log_channel(self): + user = "" + try: + user = getpass.getuser() + except KeyError: + self.queue_message( + "vv", + "Current user (uid=%s) does not seem to exist on this system, leaving user empty." + % os.getuid(), + ) + name = "p=%s u=%s | " % (os.getpid(), user) + name += "%s [%s]" % (self.ssh_type, self._play_context.remote_addr) + return name + + @ensure_connect + def get_prompt(self): + """Returns the current prompt from the device""" + return self._matched_prompt + + def exec_command(self, cmd, in_data=None, sudoable=True): + # not accessible code alert can be taken out at around 01-01-2027, + # when connection local is removed + if self._ssh_shell: + try: + cmd = json.loads(to_text(cmd, errors="surrogate_or_strict")) + kwargs = {"command": to_bytes(cmd["command"], errors="surrogate_or_strict")} + for key in ( + "prompt", + "answer", + "sendonly", + "newline", + "prompt_retry_check", + ): + if cmd.get(key) is True or cmd.get(key) is False: + kwargs[key] = cmd[key] + elif cmd.get(key) is not None: + kwargs[key] = to_bytes(cmd[key], errors="surrogate_or_strict") + return self.send(**kwargs) + except ValueError: + cmd = to_bytes(cmd, errors="surrogate_or_strict") + return self.send(command=cmd) + + else: + return super(Connection, self).exec_command(cmd, in_data, sudoable) + + def get_options(self, hostvars=None): + options = super(Connection, self).get_options(hostvars=hostvars) + options.update(self.ssh_type_conn.get_options(hostvars=hostvars)) + return options + + def set_options(self, task_keys=None, var_options=None, direct=None): + super(Connection, self).set_options( + task_keys=task_keys, var_options=var_options, direct=direct + ) + self.ssh_type_conn.set_options(task_keys=task_keys, var_options=var_options, direct=direct) + # Retain old look_for_keys behaviour, but only if not set + if not any( + [ + task_keys and ("look_for_keys" in task_keys), + var_options and ("look_for_keys" in var_options), + direct and ("look_for_keys" in direct), + ] + ): + look_for_keys = not bool( + self.get_option("password") and not self.get_option("private_key_file") + ) + if not look_for_keys: + # This actually can't be overridden yet without changes in ansible-core + # TODO: Uncomment when appropriate + # self.queue_message( + # "warning", + # "Option look_for_keys has been implicitly set to {0} because " + # "it was not set explicitly. This is retained to maintain " + # "backwards compatibility with the old behavior. This behavior " + # "will be removed in some release after 2024-01-01".format( + # look_for_keys + # ), + # ) + self.ssh_type_conn.set_option("look_for_keys", look_for_keys) + + def update_play_context(self, pc_data): + """Updates the play context information for the connection""" + pc_data = to_bytes(pc_data) + pc_data = cPickle.loads(pc_data, encoding="bytes") + + play_context = PlayContext() + play_context.deserialize(pc_data) + + self.queue_message("vvvv", "updating play_context for connection") + if self._play_context.become ^ play_context.become: + if play_context.become is True: + auth_pass = play_context.become_pass + self._on_become(become_pass=auth_pass) + self.queue_message("vvvv", "authorizing connection") + else: + self._terminal.on_unbecome() + self.queue_message("vvvv", "deauthorizing connection") + + self._play_context = play_context + if self._ssh_type_conn is not None: + # TODO: This works, but is not really ideal. We would rather use + # set_options, but then we need more custom handling in that + # method. + self._ssh_type_conn._play_context = play_context + + if hasattr(self, "reset_history"): + self.reset_history() + if hasattr(self, "disable_response_logging"): + self.disable_response_logging() + + self._single_user_mode = self.get_option("single_user_mode") + + def set_check_prompt(self, task_uuid): + self._check_prompt = task_uuid + + def update_cli_prompt_context(self): + # set cli prompt context at the start of new task run only + if self._check_prompt and self._task_uuid != self._check_prompt: + self._task_uuid, self._check_prompt = self._check_prompt, False + self.set_cli_prompt_context() + + def _connect(self): + """ + Connects to the remote device and starts the terminal + """ + if display.verbosity > 3: + logging.getLogger(self.ssh_type).setLevel(logging.DEBUG) + + self.queue_message("vvvv", "invoked shell using ssh_type: %s" % self.ssh_type) + + self._single_user_mode = self.get_option("single_user_mode") + + if not self.connected: + self.ssh_type_conn._set_log_channel(self._get_log_channel()) + self.ssh_type_conn.force_persistence = self.force_persistence + + command_timeout = self.get_option("persistent_command_timeout") + max_pause = min( + [ + self.get_option("persistent_connect_timeout"), + command_timeout, + ] + ) + retries = self.get_option("network_cli_retries") + total_pause = 0 + + for attempt in range(retries + 1): + try: + ssh = self.ssh_type_conn._connect() + break + except AnsibleError: + raise + except Exception as e: + pause = 2 ** (attempt + 1) + if attempt == retries or total_pause >= max_pause: + raise AnsibleConnectionFailure(to_text(e, errors="surrogate_or_strict")) + else: + msg = ( + "network_cli_retry: attempt: %d, caught exception(%s), " + "pausing for %d seconds" + % ( + attempt + 1, + to_text(e, errors="surrogate_or_strict"), + pause, + ) + ) + + self.queue_message("vv", msg) + time.sleep(pause) + total_pause += pause + continue + + self.queue_message("vvvv", "ssh connection done, setting terminal") + self._connected = True + + self._ssh_shell = ssh.ssh.invoke_shell() + if self.ssh_type == "paramiko": + self._ssh_shell.settimeout(command_timeout) + + self.queue_message( + "vvvv", + "loaded terminal plugin for network_os %s" % self._network_os, + ) + + terminal_initial_prompt = ( + self.get_option("terminal_initial_prompt") or self._terminal.terminal_initial_prompt + ) + terminal_initial_answer = ( + self.get_option("terminal_initial_answer") or self._terminal.terminal_initial_answer + ) + newline = ( + self.get_option("terminal_inital_prompt_newline") + or self._terminal.terminal_inital_prompt_newline + ) + check_all = self.get_option("terminal_initial_prompt_checkall") or False + + self.receive( + prompts=terminal_initial_prompt, + answer=terminal_initial_answer, + newline=newline, + check_all=check_all, + ) + + if self._play_context.become: + self.queue_message("vvvv", "firing event: on_become") + auth_pass = self._play_context.become_pass + self._on_become(become_pass=auth_pass) + + self.queue_message("vvvv", "firing event: on_open_shell()") + self._on_open_shell() + + self.queue_message("vvvv", "ssh connection has completed successfully") + + return self + + def _on_become(self, become_pass=None): + """ + Wraps terminal.on_become() to handle + privilege escalation failures based on user preference + """ + on_become_error = self.get_option("become_errors") + try: + self._terminal.on_become(passwd=become_pass) + except AnsibleConnectionFailure: + if on_become_error == "ignore": + pass + elif on_become_error == "warn": + self.queue_message("warning", "on_become: privilege escalation failed") + else: + raise + + def _on_open_shell(self): + """ + Wraps terminal.on_open_shell() to handle + terminal setting failures based on user preference + """ + on_terminal_error = self.get_option("terminal_errors") + try: + self._terminal.on_open_shell() + except AnsibleConnectionFailure: + if on_terminal_error == "ignore": + pass + elif on_terminal_error == "warn": + self.queue_message( + "warning", + "on_open_shell: failed to set terminal parameters", + ) + else: + raise + + def close(self): + """ + Close the active connection to the device + """ + # only close the connection if its connected. + if self._connected: + self.queue_message("debug", "closing ssh connection to device") + if self._ssh_shell: + self.queue_message("debug", "firing event: on_close_shell()") + self._terminal.on_close_shell() + self._ssh_shell.close() + self._ssh_shell = None + self.queue_message("debug", "cli session is now closed") + + self.ssh_type_conn.close() + self._ssh_type_conn = None + self.queue_message("debug", "ssh connection has been closed successfully") + super(Connection, self).close() + + def _read_post_command_prompt_match(self): + time.sleep(self.get_option("persistent_buffer_read_timeout")) + data = self._ssh_shell.read_bulk_response() + return data if data else None + + def receive_paramiko( + self, + command=None, + prompts=None, + answer=None, + newline=True, + prompt_retry_check=False, + check_all=False, + strip_prompt=True, + ): + recv = BytesIO() + cache_socket_timeout = self.get_option("persistent_command_timeout") + self._ssh_shell.settimeout(cache_socket_timeout) + command_prompt_matched = False + handled = False + errored_response = None + + while True: + if command_prompt_matched: + try: + signal.signal(signal.SIGALRM, self._handle_buffer_read_timeout) + signal.setitimer(signal.ITIMER_REAL, self._buffer_read_timeout) + data = self._ssh_shell.recv(256) + signal.alarm(0) + self._log_messages("response-%s: %s" % (self._window_count + 1, data)) + # if data is still received on channel it indicates the prompt string + # is wrongly matched in between response chunks, continue to read + # remaining response. + command_prompt_matched = False + + # restart command_timeout timer + signal.signal(signal.SIGALRM, self._handle_command_timeout) + signal.alarm(self._command_timeout) + + except AnsibleCmdRespRecv: + # reset socket timeout to global timeout + return self._command_response + else: + data = self._ssh_shell.recv(256) + self._log_messages("response-%s: %s" % (self._window_count + 1, data)) + # when a channel stream is closed, received data will be empty + if not data: + break + + recv.write(data) + offset = recv.tell() - 256 if recv.tell() > 256 else 0 + recv.seek(offset) + + window = self._strip(recv.read()) + self._last_recv_window = window + self._window_count += 1 + + if prompts and not handled: + handled = self._handle_prompt(window, prompts, answer, newline, False, check_all) + self._matched_prompt_window = self._window_count + elif ( + prompts + and handled + and prompt_retry_check + and self._matched_prompt_window + 1 == self._window_count + ): + # check again even when handled, if same prompt repeats in next window + # (like in the case of a wrong enable password, etc) indicates + # value of answer is wrong, report this as error. + if self._handle_prompt( + window, + prompts, + answer, + newline, + prompt_retry_check, + check_all, + ): + raise AnsibleConnectionFailure( + "For matched prompt '%s', answer is not valid" % self._matched_cmd_prompt + ) + + if self._find_error(window): + # We can't exit here, as we need to drain the buffer in case + # the error isn't fatal, and will be using the buffer again + errored_response = window + + if self._find_prompt(window): + if errored_response: + raise AnsibleConnectionFailure(errored_response) + self._last_response = recv.getvalue() + resp = self._strip(self._last_response) + self._command_response = self._sanitize(resp, command, strip_prompt) + if self._buffer_read_timeout == 0.0: + # reset socket timeout to global timeout + return self._command_response + else: + command_prompt_matched = True + + def receive_libssh( + self, + command=None, + prompts=None, + answer=None, + newline=True, + prompt_retry_check=False, + check_all=False, + strip_prompt=True, + ): + self._command_response = resp = b"" + command_prompt_matched = False + handled = False + errored_response = None + + while True: + if command_prompt_matched: + data = self._read_post_command_prompt_match() + if data: + command_prompt_matched = False + else: + return self._command_response + else: + try: + data = self._ssh_shell.read_bulk_response() + # TODO: Should be ConnectionError when pylibssh drops Python 2 support + except OSError: + # Socket has closed + break + + if not data: + continue + self._last_recv_window = self._strip(data) + resp += self._last_recv_window + self._window_count += 1 + + self._log_messages("response-%s: %s" % (self._window_count, data)) + + if prompts and not handled: + handled = self._handle_prompt(resp, prompts, answer, newline, False, check_all) + self._matched_prompt_window = self._window_count + elif ( + prompts + and handled + and prompt_retry_check + and self._matched_prompt_window + 1 == self._window_count + ): + # check again even when handled, if same prompt repeats in next window + # (like in the case of a wrong enable password, etc) indicates + # value of answer is wrong, report this as error. + if self._handle_prompt( + resp, + prompts, + answer, + newline, + prompt_retry_check, + check_all, + ): + raise AnsibleConnectionFailure( + "For matched prompt '%s', answer is not valid" % self._matched_cmd_prompt + ) + + if self._find_error(resp): + # We can't exit here, as we need to drain the buffer in case + # the error isn't fatal, and will be using the buffer again + errored_response = resp + + if self._find_prompt(resp): + if errored_response: + raise AnsibleConnectionFailure(errored_response) + self._last_response = data + self._command_response = self._sanitize(resp, command, strip_prompt) + command_prompt_matched = True + + def receive( + self, + command=None, + prompts=None, + answer=None, + newline=True, + prompt_retry_check=False, + check_all=False, + strip_prompt=True, + ): + """ + Handles receiving of output from command + """ + self._matched_prompt = None + self._matched_cmd_prompt = None + self._matched_prompt_window = 0 + self._window_count = 0 + + # set terminal regex values for command prompt and errors in response + self._terminal_stderr_re = self._get_terminal_std_re("terminal_stderr_re") + self._terminal_stdout_re = self._get_terminal_std_re("terminal_stdout_re") + + self._command_timeout = self.get_option("persistent_command_timeout") + self._validate_timeout_value(self._command_timeout, "persistent_command_timeout") + + self._buffer_read_timeout = self.get_option("persistent_buffer_read_timeout") + self._validate_timeout_value(self._buffer_read_timeout, "persistent_buffer_read_timeout") + + self._log_messages("command: %s" % command) + if self.ssh_type == "libssh": + response = self.receive_libssh( + command, + prompts, + answer, + newline, + prompt_retry_check, + check_all, + strip_prompt, + ) + elif self.ssh_type == "paramiko": + response = self.receive_paramiko( + command, + prompts, + answer, + newline, + prompt_retry_check, + check_all, + strip_prompt, + ) + + return response + + @ensure_connect + def send( + self, + command, + prompt=None, + answer=None, + newline=True, + sendonly=False, + prompt_retry_check=False, + check_all=False, + strip_prompt=True, + ): + """ + Sends the command to the device in the opened shell + """ + # try cache first + if (not prompt) and (self._single_user_mode): + out = self.get_cache().lookup(command) + if out: + self.queue_message("vvvv", "cache hit for command: %s" % command) + return out + + if check_all: + prompt_len = len(to_list(prompt)) + answer_len = len(to_list(answer)) + if prompt_len != answer_len: + raise AnsibleConnectionFailure( + "Number of prompts (%s) is not same as that of answers (%s)" + % (prompt_len, answer_len) + ) + try: + cmd = b"%s\r" % command + self._history.append(cmd) + self._ssh_shell.sendall(cmd) + self._log_messages("send command: %s" % cmd) + if sendonly: + return + response = self.receive( + command, + prompt, + answer, + newline, + prompt_retry_check, + check_all, + strip_prompt, + ) + response = to_text(response, errors="surrogate_then_replace") + + if (not prompt) and (self._single_user_mode): + if self._needs_cache_invalidation(command): + # invalidate the existing cache + if self.get_cache().keys(): + self.queue_message("vvvv", "invalidating existing cache") + self.get_cache().invalidate() + else: + # populate cache + self.queue_message("vvvv", "populating cache for command: %s" % command) + self.get_cache().populate(command, response) + + return response + except (socket.timeout, AttributeError): + self.queue_message("error", traceback.format_exc()) + raise AnsibleConnectionFailure( + "timeout value %s seconds reached while trying to send command: %s" + % (self._ssh_shell.gettimeout(), command.strip()) + ) + + def _handle_buffer_read_timeout(self, signum, frame): + self.queue_message( + "vvvv", + "Response received, triggered 'persistent_buffer_read_timeout' timer of %s seconds" + % self.get_option("persistent_buffer_read_timeout"), + ) + raise AnsibleCmdRespRecv() + + def _handle_command_timeout(self, signum, frame): + msg = ( + "command timeout triggered, timeout value is %s secs.\nSee the timeout setting options in the Network Debug and Troubleshooting Guide." + % self.get_option("persistent_command_timeout") + ) + self.queue_message("log", msg) + raise AnsibleConnectionFailure(msg) + + def _strip(self, data): + """ + Removes ANSI codes from device response + """ + for regex in self._terminal.ansi_re: + data = regex.sub(b"", data) + return data + + def _handle_prompt( + self, + resp, + prompts, + answer, + newline, + prompt_retry_check=False, + check_all=False, + ): + """ + Matches the command prompt and responds + + :arg resp: Byte string containing the raw response from the remote + :arg prompts: Sequence of byte strings that we consider prompts for input + :arg answer: Sequence of Byte string to send back to the remote if we find a prompt. + A carriage return is automatically appended to this string. + :param prompt_retry_check: Bool value for trying to detect more prompts + :param check_all: Bool value to indicate if all the values in prompt sequence should be matched or any one of + given prompt. + :returns: True if a prompt was found in ``resp``. If check_all is True + will True only after all the prompt in the prompts list are matched. False otherwise. + """ + single_prompt = False + if not isinstance(prompts, list): + prompts = [prompts] + single_prompt = True + if not isinstance(answer, list): + answer = [answer] + try: + prompts_regex = [re.compile(to_bytes(r), re.I) for r in prompts] + except re.error as exc: + raise ConnectionError( + "Failed to compile one or more terminal prompt regexes: %s.\n" + "Prompts provided: %s" % (to_text(exc), prompts) + ) + for index, regex in enumerate(prompts_regex): + match = regex.search(resp) + if match: + self._matched_cmd_prompt = match.group() + self._log_messages("matched command prompt: %s" % self._matched_cmd_prompt) + + # if prompt_retry_check is enabled to check if same prompt is + # repeated don't send answer again. + if not prompt_retry_check: + prompt_answer = to_bytes(answer[index] if len(answer) > index else answer[0]) + if newline: + prompt_answer += b"\r" + self._ssh_shell.sendall(prompt_answer) + self._log_messages("matched command prompt answer: %s" % prompt_answer) + if check_all and prompts and not single_prompt: + prompts.pop(0) + answer.pop(0) + return False + return True + return False + + def _sanitize(self, resp, command=None, strip_prompt=True): + """ + Removes elements from the response before returning to the caller + """ + cleaned = [] + for line in resp.splitlines(): + if command and line.strip() == command.strip(): + continue + + for prompt in self._matched_prompt.strip().splitlines(): + if prompt.strip() in line and strip_prompt: + break + else: + cleaned.append(line) + + return b"\n".join(cleaned).strip() + + def _find_error(self, response): + """Searches the buffered response for a matching error condition""" + for stderr_regex in self._terminal_stderr_re: + if stderr_regex.search(response): + self._log_messages( + "matched error regex (terminal_stderr_re) '%s' from response '%s'" + % (stderr_regex.pattern, response) + ) + + self._log_messages( + "matched stdout regex (terminal_stdout_re) '%s' from error response '%s'" + % (self._matched_pattern, response) + ) + return True + + return False + + def _find_prompt(self, response): + """Searches the buffered response for a matching command prompt""" + for stdout_regex in self._terminal_stdout_re: + match = stdout_regex.search(response) + if match: + self._matched_pattern = stdout_regex.pattern + self._matched_prompt = match.group() + self._log_messages( + "matched cli prompt '%s' with regex '%s' from response '%s'" + % (self._matched_prompt, self._matched_pattern, response) + ) + return True + + return False + + def _validate_timeout_value(self, timeout, timer_name): + if timeout < 0: + raise AnsibleConnectionFailure( + "'%s' timer value '%s' is invalid, value should be greater than or equal to zero." + % (timer_name, timeout) + ) + + def transport_test(self, connect_timeout): + """This method enables wait_for_connection to work. + + As it is used by wait_for_connection, it is called by that module's action plugin, + which is on the controller process, which means that nothing done on this instance + should impact the actual persistent connection... this check is for informational + purposes only and should be properly cleaned up. + """ + + # Force a fresh connect if for some reason we have connected before. + self.close() + self._connect() + self.close() + + def _get_terminal_std_re(self, option): + terminal_std_option = self.get_option(option) + terminal_std_re = [] + + if terminal_std_option: + for item in terminal_std_option: + if "pattern" not in item: + raise AnsibleConnectionFailure( + "'pattern' is a required key for option '%s'," + " received option value is %s" % (option, item) + ) + pattern = rb"%s" % to_bytes(item["pattern"]) + flag = item.get("flags", 0) + if flag: + flag = getattr(re, flag.split(".")[1]) + terminal_std_re.append(re.compile(pattern, flag)) + else: + # To maintain backward compatibility + terminal_std_re = getattr(self._terminal, option) + + return terminal_std_re + + def copy_file(self, source=None, destination=None, proto="scp", timeout=30): + """Copies file over scp/sftp to remote device + + :param source: Source file path + :param destination: Destination file path on remote device + :param proto: Protocol to be used for file transfer, + supported protocol: scp and sftp + :param timeout: Specifies the wait time to receive response from + remote host before triggering timeout exception + :return: None + """ + if self.ssh_type == "libssh": + self.ssh_type_conn.put_file(source, destination, proto=proto) + elif self.ssh_type == "paramiko": + ssh = self.ssh_type_conn._connect_uncached() + if proto == "scp": + if not HAS_SCP: + raise AnsibleError(missing_required_lib("scp")) + with SCPClient(ssh.get_transport(), socket_timeout=timeout) as scp: + scp.put(source, destination) + elif proto == "sftp": + with ssh.open_sftp() as sftp: + sftp.put(source, destination) + else: + raise AnsibleError("Do not know how to do transfer file over protocol %s" % proto) + else: + raise AnsibleError("Do not know how to do SCP with ssh_type %s" % self.ssh_type) + + def get_file(self, source=None, destination=None, proto="scp", timeout=30): + """Fetch file over scp/sftp from remote device + :param source: Source file path + :param destination: Destination file path + :param proto: Protocol to be used for file transfer, + supported protocol: scp and sftp + :param timeout: Specifies the wait time to receive response from + remote host before triggering timeout exception + :return: None + """ + if self.ssh_type == "libssh": + self.ssh_type_conn._connect() + self.ssh_type_conn.fetch_file(source, destination, proto=proto) + elif self.ssh_type == "paramiko": + ssh = self.ssh_type_conn._connect_uncached() + if proto == "scp": + if not HAS_SCP: + raise AnsibleError(missing_required_lib("scp")) + try: + with SCPClient(ssh.get_transport(), socket_timeout=timeout) as scp: + scp.get(source, destination) + except EOFError: + # This appears to be benign. + pass + elif proto == "sftp": + with ssh.open_sftp() as sftp: + sftp.get(source, destination) + else: + raise AnsibleError("Do not know how to do transfer file over protocol %s" % proto) + else: + raise AnsibleError("Do not know how to do SCP with ssh_type %s" % self.ssh_type) + + def get_cache(self): + if not self._cache: + # TO-DO: support jsonfile or other modes of caching with + # a configurable option + self._cache = cache_loader.get("ansible.netcommon.memory") + return self._cache + + def _is_in_config_mode(self): + """ + Check if the target device is in config mode by comparing + the current prompt with the platform's `terminal_config_prompt`. + Returns False if `terminal_config_prompt` is not defined. + + :returns: A boolean indicating if the device is in config mode or not. + """ + cfg_mode = False + cur_prompt = to_text(self.get_prompt(), errors="surrogate_then_replace").strip() + cfg_prompt = getattr(self._terminal, "terminal_config_prompt", None) + if cfg_prompt and cfg_prompt.match(cur_prompt): + cfg_mode = True + return cfg_mode + + def _needs_cache_invalidation(self, command): + """ + This method determines if it is necessary to invalidate + the existing cache based on whether the device has entered + configuration mode or if the last command sent to the device + is potentially capable of making configuration changes. + + :param command: The last command sent to the target device. + :returns: A boolean indicating if cache invalidation is required or not. + """ + invalidate = False + cfg_cmds = [] + try: + # AnsiblePlugin base class in Ansible 2.9 does not have has_option() method. + # TO-DO: use has_option() when we drop 2.9 support. + cfg_cmds = self.cliconf.get_option("config_commands") + except AttributeError: + cfg_cmds = [] + if (self._is_in_config_mode()) or (to_text(command) in cfg_cmds): + invalidate = True + return invalidate diff --git a/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py b/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py new file mode 100644 index 0000000..5727cf3 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/connection/persistent.py @@ -0,0 +1,87 @@ +# 2017 Red Hat Inc. +# (c) 2017 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 = """ +author: + - Ansible Networking Team (@ansible-network) +name: persistent +short_description: Use a persistent unix socket for connection +description: +- This is a helper plugin to allow making other connections persistent. +version_added: 1.0.0 +extends_documentation_fragment: +- ansible.netcommon.connection_persistent +""" +from ansible.executor.task_executor import start_connection +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import Connection as SocketConnection +from ansible.plugins.connection import ConnectionBase +from ansible.utils.display import Display + + +display = Display() + + +class Connection(ConnectionBase): + """Local based connections""" + + transport = "ansible.netcommon.persistent" + has_pipelining = False + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + self._task_uuid = to_text(kwargs.get("task_uuid", "")) + + def _connect(self): + self._connected = True + return self + + def exec_command(self, cmd, in_data=None, sudoable=True): + display.vvvv( + "exec_command(), socket_path=%s" % self.socket_path, + host=self._play_context.remote_addr, + ) + connection = SocketConnection(self.socket_path) + out = connection.exec_command(cmd, in_data=in_data, sudoable=sudoable) + return 0, out, "" + + def put_file(self, in_path, out_path): + pass + + def fetch_file(self, in_path, out_path): + pass + + def close(self): + self._connected = False + + def run(self): + """Returns the path of the persistent connection socket. + + Attempts to ensure (within playcontext.timeout seconds) that the + socket path exists. If the path exists (or the timeout has expired), + returns the socket path. + """ + display.vvvv( + "starting connection from persistent connection plugin", + host=self._play_context.remote_addr, + ) + display.deprecated( + msg="support for connection local has been deprecated", + date="2027-01-01", + collection_name="ansible.netcommon", + ) + variables = {"ansible_command_timeout": self.get_option("persistent_command_timeout")} + socket_path = start_connection(self._play_context, variables, self._task_uuid) + display.vvvv( + "local domain socket path is %s" % socket_path, + host=self._play_context.remote_addr, + ) + setattr(self, "_socket_path", socket_path) + return socket_path diff --git a/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/connection_persistent.py b/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/connection_persistent.py new file mode 100644 index 0000000..16bae27 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/connection_persistent.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# 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 + + +class ModuleDocFragment(object): + # Standard files documentation fragment + DOCUMENTATION = r""" +options: + import_modules: + type: boolean + description: + - Reduce CPU usage and network module execution time + by enabling direct execution. Instead of the module being packaged + and executed by the shell, it will be directly executed by the Ansible + control node using the same python interpreter as the Ansible process. + Note- Incompatible with C(asynchronous mode). + Note- Python 3 and Ansible 2.9.16 or greater required. + Note- With Ansible 2.9.x fully qualified modules names are required in tasks. + default: true + ini: + - section: ansible_network + key: import_modules + env: + - name: ANSIBLE_NETWORK_IMPORT_MODULES + vars: + - name: ansible_network_import_modules + persistent_connect_timeout: + type: int + description: + - Configures, in seconds, the amount of time to wait when trying to initially + establish a persistent connection. If this value expires before the connection + to the remote device is completed, the connection will fail. + default: 30 + ini: + - section: persistent_connection + key: connect_timeout + env: + - name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT + vars: + - name: ansible_connect_timeout + persistent_command_timeout: + type: int + description: + - Configures, in seconds, the amount of time to wait for a command to + return from the remote device. If this timer is exceeded before the + command returns, the connection plugin will raise an exception and + close. + default: 30 + ini: + - section: persistent_connection + key: command_timeout + env: + - name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT + vars: + - name: ansible_command_timeout + persistent_log_messages: + type: boolean + description: + - This flag will enable logging the command executed and response received from + target device in the ansible log file. For this option to work 'log_path' ansible + configuration option is required to be set to a file path with write access. + - Be sure to fully understand the security implications of enabling this + option as it could create a security vulnerability by logging sensitive information in log file. + default: False + ini: + - section: persistent_connection + key: log_messages + env: + - name: ANSIBLE_PERSISTENT_LOG_MESSAGES + vars: + - name: ansible_persistent_log_messages +""" diff --git a/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/network_agnostic.py b/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/network_agnostic.py new file mode 100644 index 0000000..878c91e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/network_agnostic.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# Copyright: (c) 2019 Ansible, 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 + + +class ModuleDocFragment(object): + # Standard files documentation fragment + DOCUMENTATION = r"""options: {} +notes: +- This module is supported on C(ansible_network_os) network platforms. See the :ref:`Network + Platform Options ` for details. +""" diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/comp_type5.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/comp_type5.py new file mode 100644 index 0000000..bb40322 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/comp_type5.py @@ -0,0 +1,113 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The comp_type5 filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: comp_type5 +author: Ken Celenza (@itdependsnetworks) +version_added: "1.0.0" +short_description: The comp_type5 filter plugin. +description: + - The filter confirms configuration idempotency on use of type5_pw. +notes: + - The filter confirms configuration idempotency on use of type5_pw. + - Can be used to validate password post hashing + username cisco secret 5 {{ ansible_ssh_pass | ansible.netcommon.comp_type5(encrypted, True) }} +options: + unencrypted_password: + description: + - The unencrypted text. + type: str + required: True + encrypted_password: + description: + - The encrypted text. + type: str + required: True + return_original: + description: + - Return the original text. + type: bool +""" + +EXAMPLES = r""" +# Using comp_type5 + +# playbook + +- name: Set the facts + ansible.builtin.set_fact: + unencrypted_password: "cisco@123" + encrypted_password: "$1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/" + +- name: Invoke comp_type5 + ansible.builtin.debug: + msg: "{{ unencrypted_password | ansible.netcommon.comp_type5(encrypted_password, False) }}" + +# Task Output +# ----------- +# +# TASK [Set the facts] +# ok: [35.155.113.92] => changed=false +# ansible_facts: +# encrypted_password: $1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/ +# unencrypted_password: cisco@123 + +# TASK [Invoke comp_type5] +# ok: [35.155.113.92] => +# msg: true +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.comp_type5 import comp_type5 + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + + +@pass_environment +def _comp_type5(*args, **kwargs): + """Extend vlan data""" + + keys = [ + "unencrypted_password", + "encrypted_password", + "return_original", + ] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + data = convert_to_native(data) + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="comp_type5") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return comp_type5(**updated_data) + + +class FilterModule(object): + """comp_type5""" + + def filters(self): + """a mapping of filter names to functions""" + return {"comp_type5": _comp_type5} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/hash_salt.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/hash_salt.py new file mode 100644 index 0000000..8d9a1cf --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/hash_salt.py @@ -0,0 +1,100 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The hash_salt filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: hash_salt +author: Ken Celenza (@itdependsnetworks) +version_added: "1.0.0" +short_description: The hash_salt filter plugin. +description: + - The filter plugin produces the salt from a hashed password. + - Using the parameters below - C(password | ansible.netcommon.hash_salt(template.yml)) +notes: + - The filter plugin produces the salt from a hashed password. +options: + password: + description: + - This source data on which hash_salt invokes. + - For example C(password | ansible.netcommon.hash_salt), + in this case C(password) represents the hashed password. + type: str + required: True +""" + +EXAMPLES = r""" +# Using hash_salt + +# playbook + +- name: Set the facts + ansible.builtin.set_fact: + password: "$1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/" + +- name: Invoke hash_salt + ansible.builtin.debug: + msg: "{{ password | ansible.netcommon.hash_salt() }}" + + +# Task Output +# ----------- +# +# TASK [Set the facts] +# ok: [host] => changed=false +# ansible_facts: +# password: $1$avs$uSTOEMh65ADDBREAKqzvpb9yBMpzd/ + +# TASK [Invoke hash_salt] +# ok: [host] => +# msg: avs +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.hash_salt import hash_salt + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + + +@pass_environment +def _hash_salt(*args, **kwargs): + """Extend vlan data""" + + keys = ["password"] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + data = convert_to_native(data) + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="hash_salt") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return hash_salt(**updated_data) + + +class FilterModule(object): + """hash_salt""" + + def filters(self): + """a mapping of filter names to functions""" + return {"hash_salt": _hash_salt} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_cli.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_cli.py new file mode 100644 index 0000000..ee11888 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_cli.py @@ -0,0 +1,175 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The parse_cli filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: parse_cli +author: Peter Sprygada (@privateip) +version_added: "1.0.0" +short_description: parse_cli filter plugin. +description: + - The filter plugins converts the output of a network device + CLI command into structured JSON output. + - Using the parameters below - C(xml_data | ansible.netcommon.parse_cli(template.yml)) + - This plugin is deprecated and will be removed in a future release after 2027-02-01, please Use ansible.utils.cli_parse instead. +notes: + - The parse_cli filter will load the spec file and pass the command output through it, + returning JSON output. The YAML spec file defines how to parse the CLI output +options: + output: + description: + - This source data on which parse_cli invokes. + type: raw + required: True + tmpl: + description: + - The spec file should be valid formatted YAML. + It defines how to parse the CLI output and return JSON data. + - For example C(cli_data | ansible.netcommon.parse_cli(template.yml)), + in this case C(cli_data) represents cli output. + type: str +""" + +EXAMPLES = r""" +# Using parse_cli + +# outputConfig + +# ip dhcp pool Data +# import all +# network 192.168.1.0 255.255.255.0 +# update dns +# default-router 192.168.1.1 +# dns-server 192.168.1.1 8.8.8.8 +# option 42 ip 192.168.1.1 +# domain-name test.local +# lease 8 + +# pconnection.yml + +# --- +# vars: +# dhcp_pool: +# name: "{{ item.name }}" +# network: "{{ item.network_ip }}" +# subnet: "{{ item.network_subnet }}" +# dns_servers: "{{ item.dns_servers_1 }}{{ item.dns_servers_2 }}" +# domain_name: "{{ item.domain_name_0 }}{{ item.domain_name_1 }}{{ item.domain_name_2 }}{{ item.domain_name_3 }}" +# options: "{{ item.options_1 }}{{ item.options_2 }}" +# lease_days: "{{ item.lease_days }}" +# lease_hours: "{{ item.lease_hours }}" +# lease_minutes: "{{ item.lease_minutes }}" + +# keys: +# dhcp_pools: +# value: "{{ dhcp_pool }}" +# items: "^ip dhcp pool ( +# ?P[^\\n]+)\\s+(?:import (?Pall)\\s*)?(?:network (?P[\\d.]+) +# (?P[\\d.]+)?\\s*)?(?:update dns\\s*)?(?:host (?P[\\d.]+) +# (?P[\\d.]+)\\s*)?(?:domain-name (?P[\\w._-]+)\\s+)? +# (?:default-router (?P[\\d.]+)\\s*)?(?:dns-server +# (?P(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name (?P[\\w._-]+)\\s+)? +# (?P(?:option [^\\n]+\\n*\\s*)*)?(?:domain-name (?P[\\w._-]+)\\s+)?(?P(?:option [^\\n]+\\n*\\s*)*)? +# (?:dns-server (?P(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name +# (?P[\\w._-]+)\\s*)?(lease (?P\\d+)(?: (?P\\d+))?(?: (?P\\d+))?\\s*)?(?:update arp)?" + +# playbook + +- name: Add config data + ansible.builtin.set_fact: + opconfig: "{{lookup('ansible.builtin.file', 'outputConfig') }}" + +- name: Parse Data + ansible.builtin.set_fact: + output: "{{ opconfig | parse_cli('pconnection.yml') }}" + + +# Task Output +# ----------- +# +# TASK [Add config data] +# ok: [host] => changed=false +# ansible_facts: +# xml: |- +# ip dhcp pool Data +# import all +# network 192.168.1.0 255.255.255.0 +# update dns +# default-router 192.168.1.1 +# dns-server 192.168.1.1 8.8.8.8 +# option 42 ip 192.168.1.1 +# domain-name test.local +# lease 8 + +# TASK [Parse Data] +# ok: [host] => changed=false +# ansible_facts: +# output: +# dhcp_pools: +# - dns_servers: 192.168.1.1 8.8.8.8 +# domain_name: test.local +# lease_days: 8 +# lease_hours: null +# lease_minutes: null +# name: Data +# network: 192.168.1.0 +# options: |- +# option 42 ip 192.168.1.1 +# subnet: 255.255.255.0 +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.parse_cli import parse_cli + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + +from ansible.utils.display import Display + + +@pass_environment +def _parse_cli(*args, **kwargs): + """parse cli""" + display = Display() + display.warning( + "The 'parse_cli' filter is deprecated and will be removed in a future release " + "after 2027-02-01. Use 'ansible.utils.cli_parse' instead." + ) + keys = ["output", "tmpl"] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + data = convert_to_native(data) + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="parse_cli") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return parse_cli(**updated_data) + + +class FilterModule(object): + """parse_cli""" + + def filters(self): + """a mapping of filter names to functions""" + return {"parse_cli": _parse_cli} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_cli_textfsm.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_cli_textfsm.py new file mode 100644 index 0000000..f6c8619 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_cli_textfsm.py @@ -0,0 +1,132 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The parse_cli_textfsm filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: parse_cli_textfsm +author: Peter Sprygada (@privateip) +version_added: "1.0.0" +short_description: parse_cli_textfsm filter plugin. +description: + - The network filters also support parsing the output of a CLI command using the TextFSM library. + To parse the CLI output with TextFSM use this filter. + - Using the parameters below - C(data | ansible.netcommon.parse_cli_textfsm(template.yml)) + - This plugin is deprecated and will be removed in a future release after 2027-02-01, please Use ansible.utils.cli_parse instead. +notes: + - Use of the TextFSM filter requires the TextFSM library to be installed. +options: + value: + description: + - This source data on which parse_cli_textfsm invokes. + type: raw + required: True + template: + description: + - The template to compare it with. + - For example C(data | ansible.netcommon.parse_cli_textfsm(template.yml)), + in this case C(data) represents this option. + type: str +""" + +EXAMPLES = r""" +# Using parse_cli_textfsm + +- name: "Fetch command output" + cisco.ios.ios_command: + commands: + - show lldp neighbors + register: lldp_output + +- name: "Invoke parse_cli_textfsm" + ansible.builtin.set_fact: + device_neighbors: "{{ lldp_output.stdout[0] | parse_cli_textfsm('~/ntc-templates/templates/cisco_ios_show_lldp_neighbors.textfsm') }}" + +- name: "Debug" + ansible.builtin.debug: + msg: "{{ device_neighbors }}" + +# Task Output +# ----------- +# +# TASK [Fetch command output] +# ok: [rtr-1] + +# TASK [Invoke parse_cli_textfsm] +# ok: [rtr-1] + +# TASK [Debug] +# ok: [rtr-1] => { +# "msg": [ +# { +# "CAPABILITIES": "R", +# "LOCAL_INTERFACE": "Gi0/0", +# "NEIGHBOR": "rtr-3", +# "NEIGHBOR_INTERFACE": "Gi0/0" +# }, +# { +# "CAPABILITIES": "R", +# "LOCAL_INTERFACE": "Gi0/1", +# "NEIGHBOR": "rtr-1", +# "NEIGHBOR_INTERFACE": "Gi0/1" +# } +# ] +# } +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.parse_cli_textfsm import ( + parse_cli_textfsm, +) + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + +from ansible.utils.display import Display + + +@pass_environment +def _parse_cli_textfsm(*args, **kwargs): + """parse textfsm""" + display = Display() + display.warning( + "The 'parse_cli_textfsm' filter is deprecated and will be removed in a future release " + "after 2027-02-01. Use 'ansible.utils.cli_parse' instead." + ) + keys = ["value", "template"] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + data = convert_to_native(data) + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="parse_cli_textfsm") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return parse_cli_textfsm(**updated_data) + + +class FilterModule(object): + """parse_cli_textfsm""" + + def filters(self): + """a mapping of filter names to functions""" + return {"parse_cli_textfsm": _parse_cli_textfsm} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_xml.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_xml.py new file mode 100644 index 0000000..8417ce2 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/parse_xml.py @@ -0,0 +1,207 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The parse_xml filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: parse_xml +author: Ganesh Nalawade (@ganeshrn) +version_added: "1.0.0" +short_description: The parse_xml filter plugin. +description: + - This filter will load the spec file and pass the command output + through it, returning JSON output. + - The YAML spec file defines how to parse the CLI output. + - This plugin is deprecated and will be removed in a future release after 2027-02-01, please Use ansible.utils.cli_parse instead. +notes: + - To convert the XML output of a network device command into structured JSON output. +options: + output: + description: + - This source xml on which parse_xml invokes. + type: raw + required: True + tmpl: + description: + - The spec file should be valid formatted YAML. + It defines how to parse the XML output and return JSON data. + - For example C(xml_data | ansible.netcommon.parse_xml(template.yml)), + in this case C(xml_data) represents xml data option. + type: str +""" + +EXAMPLES = r""" +# Using parse_xml + +# example_output.xml + +# +# +# +# +# +# +# 0/0/CPU0 +# +# true +# ntp-leap-no-warning +# +# +# ntp-mode-client +# true +#
10.1.1.1
+# 0 +#
+# -1 +#
+# +# +# ntp-mode-client +# true +#
172.16.252.29
+# 255 +#
+# 991 +#
+#
+#
+#
+#
+#
+#
+ +# parse_xml.yml + +# --- +# vars: +# ntp_peers: +# address: "{{ item.address }}" +# reachability: "{{ item.reachability}}" +# keys: +# result: +# value: "{{ ntp_peers }}" +# top: data/ntp/nodes/node/associations +# items: +# address: peer-summary-info/peer-info-common/address +# reachability: peer-summary-info/peer-info-common/reachability + + +- name: Facts setup + ansible.builtin.set_fact: + xml: "{{ lookup('file', 'example_output.xml') }}" + +- name: Parse xml invocation + ansible.builtin.debug: + msg: "{{ xml | ansible.netcommon.parse_xml('parse_xml.yml') }}" + + +# Task Output +# ----------- +# +# TASK [set xml Data] +# ok: [host] => changed=false +# ansible_facts: +# xml: |- +# +# +# +# +# +# +# 0/0/CPU0 +# +# true +# ntp-leap-no-warning +# +# +# ntp-mode-client +# true +#
10.1.1.1
+# 0 +#
+# -1 +#
+# +# +# ntp-mode-client +# true +#
172.16.252.29
+# 255 +#
+# 991 +#
+#
+#
+#
+#
+#
+#
+ +# TASK [Parse Data] +# ok: [host] => changed=false +# ansible_facts: +# output: +# result: +# - address: +# - 10.1.1.1 +# - 172.16.252.29 +# reachability: +# - '0' +# - '255' +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.parse_xml import parse_xml + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + +from ansible.utils.display import Display + + +@pass_environment +def _parse_xml(*args, **kwargs): + """parse xml""" + + display = Display() + display.warning( + "The 'parse_xml' filter is deprecated and will be removed in a future release " + "after 2027-02-01. Use 'ansible.utils.cli_parse' instead." + ) + keys = ["output", "tmpl"] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + data = convert_to_native(data) + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="parse_xml") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return parse_xml(**updated_data) + + +class FilterModule(object): + """parse_xml""" + + def filters(self): + """a mapping of filter names to functions""" + return {"parse_xml": _parse_xml} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/pop_ace.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/pop_ace.py new file mode 100644 index 0000000..24612c8 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/pop_ace.py @@ -0,0 +1,1030 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The pop_ace filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: pop_ace +author: Sagar Paul (@KB-perByte) +version_added: "5.1.0" +short_description: Remove ace entries from a acl source of truth. +description: + - This plugin removes specific keys from a provided acl data. + - Using the parameters below - C(acls_data | ansible.netcommon.pop_ace(filter_options=filter_options, match_criteria=match_criteria)) +notes: + - The filter plugin has been tested with facts collected for acls resource module on + Cisco IOSXE, IOSXR and NXOS. +options: + data: + description: + - This option represents a list of dictionaries of acls facts. + - For example C(acls_data | ansible.netcommon.pop_ace(filter_options=filter_options, match_criteria=match_criteria)), + in this case C(acls_data) represents this option. + type: raw + required: True + filter_options: + description: Specify filtering options which drives the filter plugin. + type: dict + suboptions: + remove: + description: + - Remove first removes one ace from each ACL entry on match + - Remove all is more aggressive and removes more than one on match + type: str + choices: ['first', 'all'] + default: all + failed_when: + description: + - On missing it fails when there is no match with the ACL data supplied + - On never it would never fail + type: str + choices: ['missing', 'never'] + default: missing + match_all: + description: When true ensures ace removed only when it matches all match criteria + type: bool + default: False + match_criteria: + description: Specify the matching configuration of the ACEs to remove. + type: dict + required: True + suboptions: + afi: + description: Specify afi to match + type: str + required: True + acl_name: + description: ACL name to match + type: str + source: + description: Source address/ host/ any of the ACE to match + type: str + destination: + description: Destination address/ host/ any of the ACE to natch + type: str + sequence: + description: Sequence number of the ACE to match + type: str + protocol: + description: Protocol name of the ACE to match + type: str + grant: + description: Grant type permit or deny to match + type: str +""" + +EXAMPLES = r""" +## Playbook with filter plugin example +vars: + filter_options: + match_all: true + match_criteria: + afi: "ipv4" + source: "192.0.2.0" + destination: "192.0.3.0" + acls_data: + - acls: + - aces: + - destination: + address: 192.0.3.0 + wildcard_bits: 0.0.0.255 + dscp: ef + grant: deny + protocol: icmp + protocol_options: + icmp: + traceroute: true + sequence: 10 + source: + address: 192.0.2.0 + wildcard_bits: 0.0.0.255 + ttl: + eq: 10 + - destination: + host: 198.51.110.0 + port_protocol: + eq: telnet + grant: deny + protocol: tcp + protocol_options: + tcp: + ack: true + sequence: 20 + source: + host: 198.51.100.0 + acl_type: extended + name: "110" + - aces: + - destination: + address: 198.51.101.0 + port_protocol: + eq: telnet + wildcard_bits: 0.0.0.255 + grant: deny + protocol: tcp + protocol_options: + tcp: + ack: true + sequence: 10 + source: + address: 198.51.100.0 + wildcard_bits: 0.0.0.255 + tos: + service_value: 12 + - destination: + address: 192.0.4.0 + port_protocol: + eq: www + wildcard_bits: 0.0.0.255 + dscp: ef + grant: deny + protocol: tcp + protocol_options: + tcp: + ack: true + sequence: 20 + source: + address: 192.0.3.0 + wildcard_bits: 0.0.0.255 + ttl: + lt: 20 + acl_type: extended + name: "123" + - aces: + - grant: deny + sequence: 10 + source: + host: 192.168.1.200 + - grant: deny + sequence: 20 + source: + address: 192.168.2.0 + wildcard_bits: 0.0.0.255 + acl_type: standard + name: std_acl + - aces: + - destination: + address: 192.0.3.0 + port_protocol: + eq: www + wildcard_bits: 0.0.0.255 + grant: deny + option: + traceroute: true + protocol: tcp + protocol_options: + tcp: + fin: true + sequence: 10 + source: + address: 192.0.2.0 + wildcard_bits: 0.0.0.255 + ttl: + eq: 10 + acl_type: extended + name: test + afi: ipv4 + - acls: + - aces: + - destination: + any: true + port_protocol: + eq: telnet + dscp: af11 + grant: deny + protocol: tcp + protocol_options: + tcp: + ack: true + sequence: 10 + source: + any: true + port_protocol: + eq: www + name: R1_TRAFFIC + afi: ipv6 + +tasks: + - name: Remove ace entries from a provided data + ansible.builtin.debug: + msg: "{{ acls_data | ansible.netcommon.pop_ace(filter_options=filter_options, match_criteria=match_criteria) }}" + +## Output +# PLAY [Filter plugin example pop_ace] ****************************************************************************************************************** + +# TASK [Remove ace entries from a provided data] *********************************************************************************************************** +# ok: [xe_machine] => +# msg: +# clean_acls: +# acls: +# - acls: +# - aces: +# - destination: +# host: 198.51.110.0 +# port_protocol: +# eq: telnet +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# host: 198.51.100.0 +# name: '110' +# - aces: +# - destination: +# address: 198.51.101.0 +# port_protocol: +# eq: telnet +# wildcard_bits: 0.0.0.255 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# address: 198.51.100.0 +# wildcard_bits: 0.0.0.255 +# tos: +# service_value: 12 +# - destination: +# address: 192.0.4.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# lt: 20 +# name: '123' +# - aces: +# - grant: deny +# sequence: 10 +# source: +# host: 192.168.1.200 +# - grant: deny +# sequence: 20 +# source: +# address: 192.168.2.0 +# wildcard_bits: 0.0.0.255 +# name: std_acl +# afi: ipv4 +# - acls: +# - aces: +# - destination: +# any: true +# port_protocol: +# eq: telnet +# dscp: af11 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# any: true +# port_protocol: +# eq: www +# name: R1_TRAFFIC +# afi: ipv6 +# removed_aces: +# acls: +# - acls: +# - aces: +# - destination: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: icmp +# protocol_options: +# icmp: +# traceroute: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# name: '110' +# - aces: +# - destination: +# address: 192.0.3.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# grant: deny +# option: +# traceroute: true +# protocol: tcp +# protocol_options: +# tcp: +# fin: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# name: test +# afi: ipv4 +# - acls: [] +# afi: ipv6 + + +## Playbook with workflow example +_tasks: + - name: Gather ACLs config from device existing ACLs config + cisco.ios.ios_acls: + state: gathered + register: result_gathered + + - name: Setting host facts for pop_ace filter plugin + ansible.builtin.set_fact: + acls_facts: "{{ result_gathered.gathered }}" + filter_options: + match_all: true + match_criteria: + afi: "ipv4" + source: "192.0.2.0" + destination: "192.0.3.0" + + - name: Invoke pop_ace filter plugin + ansible.builtin.set_fact: + clean_acls: "{{ acls_facts | ansible.netcommon.pop_ace(filter_options=filter_options, match_criteria=match_criteria) }}" + + - name: Override ACLs config with device existing ACLs config + cisco.ios.ios_acls: + state: overridden + config: "{{ clean_acls['clean_acls']['acls'] | from_yaml }}" + +## Output + +# PLAYBOOK: pop_ace_example.yml *********************************************** + +# PLAY [Filter plugin example pop_ace] **************************************** + +# TASK [Gather ACLs config with device existing ACLs config] ********************* +# ok: [xe_machine] => changed=false +# gathered: +# - acls: +# - aces: +# - destination: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: icmp +# protocol_options: +# icmp: +# traceroute: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# - destination: +# host: 198.51.110.0 +# port_protocol: +# eq: telnet +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# host: 198.51.100.0 +# acl_type: extended +# name: '110' +# - aces: +# - destination: +# address: 198.51.101.0 +# port_protocol: +# eq: telnet +# wildcard_bits: 0.0.0.255 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# address: 198.51.100.0 +# wildcard_bits: 0.0.0.255 +# tos: +# service_value: 12 +# - destination: +# address: 192.0.4.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# lt: 20 +# acl_type: extended +# name: '123' +# - aces: +# - grant: deny +# sequence: 10 +# source: +# host: 192.168.1.200 +# - grant: deny +# sequence: 20 +# source: +# address: 192.168.2.0 +# wildcard_bits: 0.0.0.255 +# acl_type: standard +# name: std_acl +# - aces: +# - destination: +# address: 192.0.3.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# grant: deny +# option: +# traceroute: true +# protocol: tcp +# protocol_options: +# tcp: +# fin: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# acl_type: extended +# name: test +# afi: ipv4 +# - acls: +# - aces: +# - destination: +# any: true +# port_protocol: +# eq: telnet +# dscp: af11 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# any: true +# port_protocol: +# eq: www +# name: R1_TRAFFIC +# afi: ipv6 +# invocation: +# module_args: +# config: null +# running_config: null +# state: gathered + +# TASK [Setting host facts for pop_ace filter plugin] ************************* +# ok: [xe_machine] => changed=false +# ansible_facts: +# acls_facts: +# - acls: +# - aces: +# - destination: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: icmp +# protocol_options: +# icmp: +# traceroute: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# - destination: +# host: 198.51.110.0 +# port_protocol: +# eq: telnet +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# host: 198.51.100.0 +# acl_type: extended +# name: '110' +# - aces: +# - destination: +# address: 198.51.101.0 +# port_protocol: +# eq: telnet +# wildcard_bits: 0.0.0.255 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# address: 198.51.100.0 +# wildcard_bits: 0.0.0.255 +# tos: +# service_value: 12 +# - destination: +# address: 192.0.4.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# lt: 20 +# acl_type: extended +# name: '123' +# - aces: +# - grant: deny +# sequence: 10 +# source: +# host: 192.168.1.200 +# - grant: deny +# sequence: 20 +# source: +# address: 192.168.2.0 +# wildcard_bits: 0.0.0.255 +# acl_type: standard +# name: std_acl +# - aces: +# - destination: +# address: 192.0.3.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# grant: deny +# option: +# traceroute: true +# protocol: tcp +# protocol_options: +# tcp: +# fin: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# acl_type: extended +# name: test +# afi: ipv4 +# - acls: +# - aces: +# - destination: +# any: true +# port_protocol: +# eq: telnet +# dscp: af11 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# any: true +# port_protocol: +# eq: www +# name: R1_TRAFFIC +# afi: ipv6 +# filter_options: +# match_all: true +# match_criteria: +# afi: ipv4 +# destination: 192.0.3.0 +# source: 192.0.2.0 + +# TASK [Invoke pop_ace filter plugin] ***************************************** +# ok: [xe_machine] => changed=false +# ansible_facts: +# clean_acls: +# clean_acls: +# acls: +# - acls: +# - aces: +# - destination: +# host: 198.51.110.0 +# port_protocol: +# eq: telnet +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# host: 198.51.100.0 +# name: '110' +# - aces: +# - destination: +# address: 198.51.101.0 +# port_protocol: +# eq: telnet +# wildcard_bits: 0.0.0.255 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# address: 198.51.100.0 +# wildcard_bits: 0.0.0.255 +# tos: +# service_value: 12 +# - destination: +# address: 192.0.4.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# lt: 20 +# name: '123' +# - aces: +# - grant: deny +# sequence: 10 +# source: +# host: 192.168.1.200 +# - grant: deny +# sequence: 20 +# source: +# address: 192.168.2.0 +# wildcard_bits: 0.0.0.255 +# name: std_acl +# afi: ipv4 +# - acls: +# - aces: +# - destination: +# any: true +# port_protocol: +# eq: telnet +# dscp: af11 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# any: true +# port_protocol: +# eq: www +# name: R1_TRAFFIC +# afi: ipv6 +# removed_aces: +# acls: +# - acls: +# - aces: +# - destination: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: icmp +# protocol_options: +# icmp: +# traceroute: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# name: '110' +# - aces: +# - destination: +# address: 192.0.3.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# grant: deny +# option: +# traceroute: true +# protocol: tcp +# protocol_options: +# tcp: +# fin: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# name: test +# afi: ipv4 +# - acls: [] +# afi: ipv6 + +# TASK [Override ACLs config with device existing ACLs config] ******************* +# changed: [xe_machine] => changed=true +# after: +# - acls: +# - aces: +# - destination: +# host: 198.51.110.0 +# port_protocol: +# eq: telnet +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# host: 198.51.100.0 +# acl_type: extended +# name: '110' +# - aces: +# - destination: +# address: 198.51.101.0 +# port_protocol: +# eq: telnet +# wildcard_bits: 0.0.0.255 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# address: 198.51.100.0 +# wildcard_bits: 0.0.0.255 +# tos: +# service_value: 12 +# - destination: +# address: 192.0.4.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# lt: 20 +# acl_type: extended +# name: '123' +# - aces: +# - grant: deny +# sequence: 10 +# source: +# host: 192.168.1.200 +# - grant: deny +# sequence: 20 +# source: +# address: 192.168.2.0 +# wildcard_bits: 0.0.0.255 +# acl_type: standard +# name: std_acl +# afi: ipv4 +# - acls: +# - aces: +# - destination: +# any: true +# port_protocol: +# eq: telnet +# dscp: af11 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# any: true +# port_protocol: +# eq: www +# name: R1_TRAFFIC +# afi: ipv6 +# before: +# - acls: +# - aces: +# - destination: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: icmp +# protocol_options: +# icmp: +# traceroute: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# - destination: +# host: 198.51.110.0 +# port_protocol: +# eq: telnet +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# host: 198.51.100.0 +# acl_type: extended +# name: '110' +# - aces: +# - destination: +# address: 198.51.101.0 +# port_protocol: +# eq: telnet +# wildcard_bits: 0.0.0.255 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# address: 198.51.100.0 +# wildcard_bits: 0.0.0.255 +# tos: +# service_value: 12 +# - destination: +# address: 192.0.4.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# dscp: ef +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 20 +# source: +# address: 192.0.3.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# lt: 20 +# acl_type: extended +# name: '123' +# - aces: +# - grant: deny +# sequence: 10 +# source: +# host: 192.168.1.200 +# - grant: deny +# sequence: 20 +# source: +# address: 192.168.2.0 +# wildcard_bits: 0.0.0.255 +# acl_type: standard +# name: std_acl +# - aces: +# - destination: +# address: 192.0.3.0 +# port_protocol: +# eq: www +# wildcard_bits: 0.0.0.255 +# grant: deny +# option: +# traceroute: true +# protocol: tcp +# protocol_options: +# tcp: +# fin: true +# sequence: 10 +# source: +# address: 192.0.2.0 +# wildcard_bits: 0.0.0.255 +# ttl: +# eq: 10 +# acl_type: extended +# name: test +# afi: ipv4 +# - acls: +# - aces: +# - destination: +# any: true +# port_protocol: +# eq: telnet +# dscp: af11 +# grant: deny +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# sequence: 10 +# source: +# any: true +# port_protocol: +# eq: www +# name: R1_TRAFFIC +# afi: ipv6 +# commands: +# - ip access-list extended 110 +# - no 10 deny icmp 192.0.2.0 0.0.0.255 192.0.3.0 0.0.0.255 traceroute dscp ef ttl eq 10 +# - no ip access-list extended test +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.pop_ace import pop_ace + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + + +@pass_environment +def _pop_ace(*args, **kwargs): + """remove ace entries from a acl data""" + + keys = ["data", "filter_options", "match_criteria"] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + data = convert_to_native(data) + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="pop_ace") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return pop_ace(**updated_data) + + +class FilterModule(object): + """pop_ace""" + + def filters(self): + """a mapping of filter names to functions""" + return {"pop_ace": _pop_ace} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/type5_pw.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/type5_pw.py new file mode 100644 index 0000000..a4b1098 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/type5_pw.py @@ -0,0 +1,101 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The type5_pw filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: type5_pw +author: Ken Celenza (@itdependsnetworks) +version_added: "1.0.0" +short_description: The type5_pw filter plugin. +description: + - Filter plugin to produce cisco type5 hashed password. + - Using the parameters below - C(xml_data | ansible.netcommon.type5_pw(template.yml)) + - This plugin uses do_encrypt if used with ansible-core 2.20+ and passlib_or_crypt for versions before 2.20 +notes: + - The filter plugin generates cisco type5 hashed password. +options: + password: + description: + - The password to be hashed. + type: str + required: True + salt: + description: + - Mention the salt to hash the password. + type: str +""" + +EXAMPLES = r""" +# Using type5_pw + +- name: Set some facts + ansible.builtin.set_fact: + password: "cisco@123" + +- name: Filter type5_pw invocation + ansible.builtin.debug: + msg: "{{ password | ansible.netcommon.type5_pw(salt='avs') }}" + + +# Task Output +# ----------- +# +# TASK [Set some facts] +# ok: [host] => changed=false +# ansible_facts: +# password: cisco@123 + +# TASK [Filter type5_pw invocation] +# ok: [host] => +# msg: $1$avs$uSTOEMh65qzvpb9yBMpzd/ +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.type5_pw import type5_pw + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + + +@pass_environment +def _type5_pw(*args, **kwargs): + """Extend vlan data""" + + keys = ["password", "salt"] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + data = convert_to_native(data) + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="type5_pw") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return type5_pw(**updated_data) + + +class FilterModule(object): + """type5_pw""" + + def filters(self): + """a mapping of filter names to functions""" + return {"type5_pw": _type5_pw} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/vlan_expander.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/vlan_expander.py new file mode 100644 index 0000000..db4ab81 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/vlan_expander.py @@ -0,0 +1,105 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The vlan_expander filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: vlan_expander +author: Akira Yokochi (@akira6592) +version_added: "2.3.0" +short_description: The vlan_expander filter plugin. +description: + - Expand shorthand list of VLANs to list all VLANs. Inverse of vlan_parser + - Using the parameters below - C(vlans_data | ansible.netcommon.vlan_expander) +notes: + - The filter plugin extends vlans when data provided in range or comma separated. +options: + data: + description: + - This option represents a string containing the range of vlans. + type: str + required: True +""" + +EXAMPLES = r""" +# Using vlan_expander + +- name: Setting host facts for vlan_expander filter plugin + ansible.builtin.set_fact: + vlan_ranges: "1,10-12,15,20-22" + +- name: Invoke vlan_expander filter plugin + ansible.builtin.set_fact: + extended_vlans: "{{ vlan_ranges | ansible.netcommon.vlan_expander }}" + + +# Task Output +# ----------- +# +# TASK [Setting host facts for vlan_expander filter plugin] +# ok: [host] => changed=false +# ansible_facts: +# vlan_ranges: 1,10-12,15,20-22 + +# TASK [Invoke vlan_expander filter plugin] +# ok: [host] => changed=false +# ansible_facts: +# extended_vlans: +# - 1 +# - 10 +# - 11 +# - 12 +# - 15 +# - 20 +# - 21 +# - 22 +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.vlan_expander import vlan_expander + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + + +@pass_environment +def _vlan_expander(*args, **kwargs): + """Extend vlan data""" + + keys = ["data"] + data = dict(zip(keys, args[1:])) + data.update(kwargs) + data = convert_to_native(data) + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="vlan_expander") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return vlan_expander(**updated_data) + + +class FilterModule(object): + """vlan_expander""" + + def filters(self): + """a mapping of filter names to functions""" + return {"vlan_expander": _vlan_expander} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/filter/vlan_parser.py b/collections/ansible_collections/ansible/netcommon/plugins/filter/vlan_parser.py new file mode 100644 index 0000000..e4cdc0c --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/filter/vlan_parser.py @@ -0,0 +1,145 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The vlan_parser filter plugin +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +name: vlan_parser +author: Steve Dodd (@idahood) +version_added: "1.0.0" +short_description: The vlan_parser filter plugin. +description: + - The filter plugin converts a list of vlans to IOS like vlan configuration. + - Converts list to a list of range of numbers into multiple lists. + - C(vlans_data | ansible.netcommon.vlan_parser(first_line_len = 20, other_line_len=20)) +notes: + - The filter plugin extends vlans when data provided in range or comma separated. +options: + data: + description: + - This option represents a list containing vlans. + type: list + required: True + first_line_len: + description: + - The first line of the list can be first_line_len characters long. + type: int + default: 48 + other_line_len: + description: + - The subsequent list lines can be other_line_len characters. + type: int + default: 44 +""" + +EXAMPLES = r""" +# Using vlan_parser + +- name: Setting host facts for vlan_parser filter plugin + ansible.builtin.set_fact: + vlans: + [ + 100, + 1688, + 3002, + 3003, + 3004, + 3005, + 3102, + 3103, + 3104, + 3105, + 3802, + 3900, + 3998, + 3999, + ] + +- name: Invoke vlan_parser filter plugin + ansible.builtin.set_fact: + vlans_ranges: "{{ vlans | ansible.netcommon.vlan_parser(first_line_len = 20, other_line_len=20) }}" + + +# Task Output +# ----------- +# +# TASK [Setting host facts for vlan_parser filter plugin] +# ok: [host] => changed=false +# ansible_facts: +# vlans: +# - 100 +# - 1688 +# - 3002 +# - 3003 +# - 3004 +# - 3005 +# - 3102 +# - 3103 +# - 3104 +# - 3105 +# - 3802 +# - 3900 +# - 3998 +# - 3999 + +# TASK [Invoke vlan_parser filter plugin] +# ok: [host] => changed=false +# ansible_facts: +# msg: +# - 100,1688,3002-3005 +# - 3102-3105,3802,3900 +# - 3998,3999 +""" + +from ansible.errors import AnsibleFilterError +from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( + AnsibleArgSpecValidator, +) + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.argspec_filter_utils import ( + convert_to_native, +) +from ansible_collections.ansible.netcommon.plugins.plugin_utils.vlan_parser import vlan_parser + + +try: + from jinja2.filters import pass_environment +except ImportError: + from jinja2.filters import environmentfilter as pass_environment + + +@pass_environment +def _vlan_parser(*args, **kwargs): + """Extend vlan data""" + # Extract filter arguments (skip environment which is first arg) + filter_args = args[1:] if args else [] + keys = ["data", "first_line_len", "other_line_len"] + data = dict(zip(keys, filter_args)) + data.update(kwargs) + + # Convert to native Python types so ArgumentSpecValidator can deepcopy (Ansible 2.19+) + data = convert_to_native(data) + + aav = AnsibleArgSpecValidator(data=data, schema=DOCUMENTATION, name="vlan_parser") + valid, errors, updated_data = aav.validate() + if not valid: + raise AnsibleFilterError(errors) + return vlan_parser(**updated_data) + + +class FilterModule(object): + """vlan_parser""" + + def filters(self): + """a mapping of filter names to functions""" + return {"vlan_parser": _vlan_parser} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/httpapi/restconf.py b/collections/ansible_collections/ansible/netcommon/plugins/httpapi/restconf.py new file mode 100644 index 0000000..e25edb4 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/httpapi/restconf.py @@ -0,0 +1,81 @@ +# Copyright (c) 2018 Cisco and/or its affiliates. +# 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: + - Ansible Networking Team (@ansible-network) +name: restconf +short_description: HttpApi Plugin for devices supporting Restconf API +description: +- This HttpApi plugin provides methods to connect to Restconf API endpoints. +version_added: 1.0.0 +options: + root_path: + type: str + description: + - Specifies the location of the Restconf root. + default: /restconf + vars: + - name: ansible_httpapi_restconf_root +""" + +import json + +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import ConnectionError +from ansible.module_utils.six.moves.urllib.error import HTTPError + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.httpapi_base import HttpApiBase + + +CONTENT_TYPE = "application/yang-data+json" + + +class HttpApi(HttpApiBase): + def send_request(self, data, **message_kwargs): + if data: + data = json.dumps(data) + + path = "/".join( + [ + self.get_option("root_path").rstrip("/"), + message_kwargs.get("path", "").lstrip("/"), + ] + ) + + headers = { + "Content-Type": message_kwargs.get("content_type") or CONTENT_TYPE, + "Accept": message_kwargs.get("accept") or CONTENT_TYPE, + } + response, response_data = self.connection.send( + path, data, headers=headers, method=message_kwargs.get("method") + ) + + return handle_response(response, response_data) + + +def handle_response(response, response_data): + try: + response_data = json.loads(response_data.read()) + except ValueError: + response_data.seek(0) + response_data = response_data.read() + + if isinstance(response, HTTPError): + if response_data: + if "errors" in response_data: + errors = response_data["errors"]["error"] + error_text = "\n".join((error["error-message"] for error in errors)) + else: + error_text = response_data + + raise ConnectionError(error_text, code=response.code) + raise ConnectionError(to_text(response), code=response.code) + + return response_data diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/cli_parser/cli_parserbase.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/cli_parser/cli_parserbase.py new file mode 100644 index 0000000..1ec406d --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/cli_parser/cli_parserbase.py @@ -0,0 +1,32 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2020 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 + +""" +The base class for cli_parsers +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# TODO: Remove this file after ansible.netcommon.cli_parse module is removed +# since this class is moved in ansible.utils collection. + + +class CliParserBase: + """The base class for cli parsers + Provides a _debug function to normalize parser debug output + """ + + def __init__(self, task_args, task_vars, debug): + self._debug = debug + self._task_args = task_args + self._task_vars = task_vars diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/cli_parser/cli_parsertemplate.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/cli_parser/cli_parsertemplate.py new file mode 100644 index 0000000..c443327 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/cli_parser/cli_parsertemplate.py @@ -0,0 +1,31 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2020 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 + +"""A shim class for the NetworkTemplate +this was done in case there is a need to +modify the resource module parser class +or extend it a split it from the cli parsers. +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class CliParserTemplate(NetworkTemplate): + """The parser template base class""" + + def __init__(self, lines=None): + super(CliParserTemplate, self).__init__(lines=lines, tmplt=self) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py new file mode 100644 index 0000000..0171661 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py @@ -0,0 +1,32 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# 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 +""" +The base class for all resource modules +""" + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network import ( + get_resource_connection, +) + + +class ConfigBase(object): + """The base class for all resource modules""" + + ACTION_STATES = ["merged", "replaced", "overridden", "deleted", "purged"] + + def __init__(self, module): + self._module = module + self.state = module.params["state"] + self._connection = None + + if self.state not in ["rendered", "parsed"]: + self._connection = get_resource_connection(module) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py new file mode 100644 index 0000000..27df20f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py @@ -0,0 +1,475 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2016 Red Hat Inc. +# +# 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 hashlib +import re + +from ansible.module_utils.common.text.converters import to_bytes, to_native +from ansible.module_utils.six.moves import zip + + +DEFAULT_COMMENT_TOKENS = ["#", "!", "/*", "*/", "echo"] + +DEFAULT_IGNORE_LINES_RE = set( + [ + re.compile(r"Using \d+ out of \d+ bytes"), + re.compile(r"Building configuration"), + re.compile(r"Current configuration : \d+ bytes"), + ] +) + + +try: + Pattern = re._pattern_type +except AttributeError: + Pattern = re.Pattern + + +class ConfigLine(object): + def __init__(self, raw): + self.text = str(raw).strip() + self.raw = raw + self._children = list() + self._parents = list() + + def __str__(self): + return self.raw + + def __eq__(self, other): + return self.line == other.line + + def __ne__(self, other): + return not self.__eq__(other) + + def __getitem__(self, key): + for item in self._children: + if item.text == key: + return item + raise KeyError(key) + + @property + def line(self): + line = self.parents + line.append(self.text) + return " ".join(line) + + @property + def children(self): + return _obj_to_text(self._children) + + @property + def child_objs(self): + return self._children + + @property + def parents(self): + return _obj_to_text(self._parents) + + @property + def path(self): + config = _obj_to_raw(self._parents) + config.append(self.raw) + return "\n".join(config) + + @property + def has_children(self): + return len(self._children) > 0 + + @property + def has_parents(self): + return len(self._parents) > 0 + + def add_child(self, obj): + if not isinstance(obj, ConfigLine): + raise AssertionError("child must be of type `ConfigLine`") + self._children.append(obj) + + +def ignore_line(text, tokens=None): + for item in tokens or DEFAULT_COMMENT_TOKENS: + if text.startswith(item): + return True + for regex in DEFAULT_IGNORE_LINES_RE: + if regex.match(text): + return True + + +def _obj_to_text(x): + return [o.text for o in x] + + +def _obj_to_raw(x): + return [o.raw for o in x] + + +def _obj_to_block(objects, visited=None): + items = list() + for o in objects: + if o not in items: + items.append(o) + for child in o._children: + if child not in items: + items.append(child) + return _obj_to_raw(items) + + +def dumps(objects, output="block", comments=False): + if output == "block": + items = _obj_to_block(objects) + elif output == "commands": + items = _obj_to_text(objects) + elif output == "raw": + items = _obj_to_raw(objects) + else: + raise TypeError("unknown value supplied for keyword output") + + if output == "block": + if comments: + for index, item in enumerate(items): + nextitem = index + 1 + if ( + nextitem < len(items) + and not item.startswith(" ") + and items[nextitem].startswith(" ") + ): + item = "!\n%s" % item + items[index] = item + items.append("!") + items.append("end") + + return "\n".join(items) + + +class NetworkConfig(object): + def __init__(self, indent=1, contents=None, comment_tokens=None, ignore_lines=None): + self._indent = indent + self._items = list() + self._config_text = None + self.comment_tokens = comment_tokens + + if ignore_lines: + for item in ignore_lines: + if not isinstance(item, Pattern): + item = re.compile(item) + DEFAULT_IGNORE_LINES_RE.add(item) + + if contents: + self.load(contents) + + @property + def items(self): + return self._items + + @property + def config_text(self): + return self._config_text + + @property + def sha1(self): + sha1 = hashlib.sha1() + sha1.update(to_bytes(str(self), errors="surrogate_or_strict")) + return sha1.digest() + + def __getitem__(self, key): + for line in self: + if line.text == key: + return line + raise KeyError(key) + + def __iter__(self): + return iter(self._items) + + def __str__(self): + return "\n".join([c.raw for c in self.items]) + + def __len__(self): + return len(self._items) + + def load(self, s): + self._config_text = s + self._items = self.parse(s) + + def loadfp(self, fp): + with open(fp) as f: + return self.load(f.read()) + + def parse(self, lines): + toplevel = re.compile(r"\S") + childline = re.compile(r"^\s*(.+)$") + entry_reg = re.compile(r"([{};])") + + ancestors = list() + config = list() + + indents = [0] + + for linenum, line in enumerate(to_native(lines, errors="surrogate_or_strict").split("\n")): + text = entry_reg.sub("", line).strip() + + cfg = ConfigLine(line) + + if not text or ignore_line(text, self.comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + indents = [0] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + + if line_indent < indents[-1]: + while indents[-1] > line_indent: + indents.pop() + + if line_indent > indents[-1]: + indents.append(line_indent) + + curlevel = len(indents) - 1 + parent_level = curlevel - 1 + + cfg._parents = ancestors[:curlevel] + + if curlevel > len(ancestors): + config.append(cfg) + continue + + for i in range(curlevel, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].add_child(cfg) + + config.append(cfg) + + return config + + def get_object(self, path): + for item in self.items: + if item.text == path[-1]: + if item.parents == path[:-1]: + return item + + def get_block(self, path): + if not isinstance(path, list): + raise AssertionError("path argument must be a list object") + obj = self.get_object(path) + if not obj: + raise ValueError("path does not exist in config") + return self._expand_block(obj) + + def get_block_config(self, path): + block = self.get_block(path) + return dumps(block, "block") + + def _expand_block(self, configobj, S=None): + if S is None: + S = list() + S.append(configobj) + for child in configobj._children: + if child in S: + continue + self._expand_block(child, S) + return S + + def _diff_line(self, other): + updates = list() + for item in self.items: + if item not in other: + updates.append(item) + return updates + + def _diff_strict(self, other): + updates = list() + # block extracted from other does not have all parents + # but the last one. In case of multiple parents we need + # to add additional parents. + if other and isinstance(other, list) and len(other) > 0: + start_other = other[0] + if start_other.parents: + for parent in start_other.parents: + other.insert(0, ConfigLine(parent)) + for index, line in enumerate(self.items): + try: + if str(line).strip() != str(other[index]).strip(): + updates.append(line) + except (AttributeError, IndexError): + updates.append(line) + return updates + + def _diff_exact(self, other): + updates = list() + if len(other) != len(self.items): + updates.extend(self.items) + else: + for ours, theirs in zip(self.items, other): + if ours != theirs: + updates.extend(self.items) + break + return updates + + def difference(self, other, match="line", path=None, replace=None): + """Perform a config diff against the another network config + + :param other: instance of NetworkConfig to diff against + :param match: type of diff to perform. valid values are 'line', + 'strict', 'exact' + :param path: context in the network config to filter the diff + :param replace: the method used to generate the replacement lines. + valid values are 'block', 'line' + + :returns: a string of lines that are different + """ + if path and match != "line": + try: + other = other.get_block(path) + except ValueError: + other = list() + else: + other = other.items + + # generate a list of ConfigLines that aren't in other + meth = getattr(self, "_diff_%s" % match) + updates = meth(other) + + if replace == "block": + parents = list() + for item in updates: + if not item.has_parents: + parents.append(item) + else: + for p in item._parents: + if p not in parents: + parents.append(p) + + updates = list() + for item in parents: + updates.extend(self._expand_block(item)) + + visited = set() + expanded = list() + + for curr_elem in updates: + add_parents = False + if expanded: + last_elem = expanded[-1] + # If parent of current line not added in expanded list flag it + # to be added later on + if ( + all([curr_elem.has_parents, last_elem.has_parents]) + and curr_elem.parents[0] != last_elem.parents[0] + ): + add_parents = True + # check if parent of current line is already added, if added don't + # add again + if last_elem.has_children and last_elem.children[0] != curr_elem.text: + add_parents = True + for p in curr_elem._parents: + if p.line not in visited or add_parents: + visited.add(p.line) + expanded.append(p) + expanded.append(curr_elem) + visited.add(curr_elem.line) + + return expanded + + def add(self, lines, parents=None): + ancestors = list() + offset = 0 + obj = None + + # global config command + if not parents: + for line in lines: + # handle ignore lines + if ignore_line(line, self.comment_tokens): + continue + + item = ConfigLine(line) + item.raw = line + if item not in self.items: + self.items.append(item) + + else: + for index, p in enumerate(parents): + try: + i = index + 1 + obj = self.get_block(parents[:i])[0] + ancestors.append(obj) + + except ValueError: + # add parent to config + offset = index * self._indent + obj = ConfigLine(p) + obj.raw = p.rjust(len(p) + offset) + if ancestors: + obj._parents = list(ancestors) + ancestors[-1]._children.append(obj) + self.items.append(obj) + ancestors.append(obj) + + # add child objects + for line in lines: + # handle ignore lines + if ignore_line(line, self.comment_tokens): + continue + + # check if child already exists + for child in ancestors[-1]._children: + if child.text == line: + break + else: + offset = len(parents) * self._indent + item = ConfigLine(line) + item.raw = line.rjust(len(line) + offset) + item._parents = ancestors + ancestors[-1]._children.append(item) + self.items.append(item) + + +class CustomNetworkConfig(NetworkConfig): + def items_text(self): + return [item.text for item in self.items] + + def expand_section(self, configobj, S=None): + if S is None: + S = list() + S.append(configobj) + for child in configobj.child_objs: + if child in S: + continue + self.expand_section(child, S) + return S + + def to_block(self, section): + return "\n".join([item.raw for item in section]) + + def get_section(self, path): + try: + section = self.get_section_objects(path) + return self.to_block(section) + except ValueError: + return list() + + def get_section_objects(self, path): + if not isinstance(path, list): + path = [path] + obj = self.get_object(path) + if not obj: + raise ValueError("path does not exist in config") + return self.expand_section(obj) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py new file mode 100644 index 0000000..9ea5aff --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py @@ -0,0 +1,154 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# 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 +""" +The facts base class +this contains methods common to all facts subsets +""" +from ansible.module_utils.common.text.converters import to_text + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network import ( + get_resource_connection, +) + + +class FactsBase(object): + """ + The facts base class + """ + + def __init__(self, module): + self._module = module + self._warnings = [] + self._gather_subset = module.params.get("gather_subset") + self._gather_network_resources = module.params.get("gather_network_resources") + self._connection = None + if module.params.get("state") not in ["rendered", "parsed"]: + self._connection = get_resource_connection(module) + + self.ansible_facts = {"ansible_network_resources": {}} + self.ansible_facts["ansible_net_gather_network_resources"] = list() + self.ansible_facts["ansible_net_gather_subset"] = list() + + if not self._gather_subset: + self._gather_subset = ["!config"] + if not self._gather_network_resources: + self._gather_network_resources = ["!all"] + + def gen_runable(self, subsets, valid_subsets, resource_facts=False): + """Generate the runable subset + + :param module: The module instance + :param subsets: The provided subsets + :param valid_subsets: The valid subsets + :param resource_facts: A boolean flag + :rtype: list + :returns: The runable subsets + """ + runable_subsets = set() + exclude_subsets = set() + minimal_gather_subset = set() + if not resource_facts: + minimal_gather_subset = frozenset(["default"]) + + for subset in subsets: + if subset == "all": + runable_subsets.update(valid_subsets) + continue + if subset == "min" and minimal_gather_subset: + runable_subsets.update(minimal_gather_subset) + continue + if subset.startswith("!"): + subset = subset[1:] + if subset == "min": + exclude_subsets.update(minimal_gather_subset) + continue + if subset == "all": + exclude_subsets.update(valid_subsets - minimal_gather_subset) + continue + exclude = True + else: + exclude = False + + if subset not in valid_subsets: + self._module.fail_json( + msg="Subset must be one of [%s], got %s" + % (", ".join(sorted(valid_subsets)), 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) + return runable_subsets + + def get_network_resources_facts( + self, facts_resource_obj_map, resource_facts_type=None, data=None + ): + """ + :param fact_resource_subsets: + :param data: previously collected configuration + :return: + """ + if not resource_facts_type: + resource_facts_type = self._gather_network_resources + + restorun_subsets = self.gen_runable( + resource_facts_type, + frozenset(facts_resource_obj_map.keys()), + resource_facts=True, + ) + if restorun_subsets: + self.ansible_facts["ansible_net_gather_network_resources"] = list(restorun_subsets) + instances = list() + for key in restorun_subsets: + fact_cls_obj = facts_resource_obj_map.get(key) + if fact_cls_obj: + instances.append(fact_cls_obj(self._module)) + else: + self._warnings.extend( + ["network resource fact gathering for '%s' is not supported" % key] + ) + + for inst in instances: + try: + inst.populate_facts(self._connection, self.ansible_facts, data) + except Exception as exc: + self._module.fail_json(msg=to_text(exc)) + + def get_network_legacy_facts(self, fact_legacy_obj_map, legacy_facts_type=None): + if not legacy_facts_type: + legacy_facts_type = self._gather_subset + + runable_subsets = self.gen_runable(legacy_facts_type, frozenset(fact_legacy_obj_map.keys())) + if runable_subsets: + facts = dict() + # default subset should always returned be with legacy facts subsets + if "default" not in runable_subsets: + runable_subsets.add("default") + self.ansible_facts["ansible_net_gather_subset"] = list(runable_subsets) + + instances = list() + for key in runable_subsets: + instances.append(fact_legacy_obj_map[key](self._module)) + + for inst in instances: + inst.populate() + facts.update(inst.facts) + self._warnings.extend(inst.warnings) + + for key, value in facts.items(): + key = "ansible_net_%s" % key + self.ansible_facts[key] = value diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py new file mode 100644 index 0000000..733a350 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py @@ -0,0 +1,157 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2017 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 sys + +from ansible.module_utils.common.text.converters import to_bytes, to_text +from ansible.module_utils.connection import Connection, ConnectionError + + +try: + from ncclient.xml_ import NCElement, new_ele, sub_ele + + HAS_NCCLIENT = True +except (ImportError, AttributeError): + HAS_NCCLIENT = False + +try: + from lxml.etree import Element, XMLSyntaxError, fromstring +except ImportError: + from xml.etree.ElementTree import Element, fromstring + + if sys.version_info < (2, 7): + from xml.parsers.expat import ExpatError as XMLSyntaxError + else: + from xml.etree.ElementTree import ParseError as XMLSyntaxError + +NS_MAP = {"nc": "urn:ietf:params:xml:ns:netconf:base:1.0"} + + +def exec_rpc(module, *args, **kwargs): + connection = NetconfConnection(module._socket_path) + return connection.execute_rpc(*args, **kwargs) + + +class NetconfConnection(Connection): + def __init__(self, socket_path): + super(NetconfConnection, self).__init__(socket_path) + + def __rpc__(self, name, *args, **kwargs): + """Executes the json-rpc and returns the output received + from remote device. + :name: rpc method to be executed over connection plugin that implements jsonrpc 2.0 + :args: Ordered list of params passed as arguments to rpc method + :kwargs: Dict of valid key, value pairs passed as arguments to rpc method + + For usage refer the respective connection plugin docs. + """ + self.check_rc = kwargs.pop("check_rc", True) + self.ignore_warning = kwargs.pop("ignore_warning", True) + + response = self._exec_jsonrpc(name, *args, **kwargs) + if "error" in response: + rpc_error = response["error"].get("data") + return self.parse_rpc_error(to_bytes(rpc_error, errors="surrogate_then_replace")) + + return fromstring(to_bytes(response["result"], errors="surrogate_then_replace")) + + def parse_rpc_error(self, rpc_error): + if self.check_rc: + try: + error_root = fromstring(rpc_error) + root = Element("root") + root.append(error_root) + + error_list = root.findall(".//nc:rpc-error", NS_MAP) + if not error_list: + raise ConnectionError(to_text(rpc_error, errors="surrogate_then_replace")) + + warnings = [] + for error in error_list: + message_ele = error.find("./nc:error-message", NS_MAP) + + if message_ele is None: + message_ele = error.find("./nc:error-info", NS_MAP) + + message = message_ele.text if message_ele is not None else None + + severity = error.find("./nc:error-severity", NS_MAP).text + + if severity == "warning" and self.ignore_warning and message is not None: + warnings.append(message) + else: + raise ConnectionError(to_text(rpc_error, errors="surrogate_then_replace")) + return warnings + except XMLSyntaxError: + raise ConnectionError(rpc_error) + + +def transform_reply(): + return b""" + + + + + + + + + + + + + + + + + + + + + """ + + +# Note: Workaround for ncclient 0.5.3 +def remove_namespaces(data): + if not HAS_NCCLIENT: + raise ImportError( + "ncclient is required but does not appear to be installed. " + "It can be installed using `pip install ncclient`" + ) + return NCElement( + to_text(data, errors="surrogate_then_replace").strip(), + transform_reply(), + ).data_xml + + +def build_root_xml_node(tag): + return new_ele(tag) + + +def build_child_xml_node(parent, tag, text=None, attrib=None): + element = sub_ele(parent, tag) + if text: + element.text = to_text(text) + if attrib: + element.attrib.update(attrib) + return element + + +def build_subtree(parent, path): + element = parent + for field in path.split("/"): + sub_element = build_child_xml_node(element, field) + element = sub_element + return element diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py new file mode 100644 index 0000000..fed043f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py @@ -0,0 +1,233 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c) 2015 Peter Sprygada, +# +# 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 +import traceback + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.common.text.converters import to_native, to_text +from ansible.module_utils.connection import Connection, ConnectionError + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import ( + NetconfConnection, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import Cli +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list + + +NET_TRANSPORT_ARGS = dict( + host=dict(required=True), + port=dict(type="int"), + username=dict(fallback=(env_fallback, ["ANSIBLE_NET_USERNAME"])), + password=dict(no_log=True, fallback=(env_fallback, ["ANSIBLE_NET_PASSWORD"])), + ssh_keyfile=dict(fallback=(env_fallback, ["ANSIBLE_NET_SSH_KEYFILE"]), type="path"), + authorize=dict( + default=False, + fallback=(env_fallback, ["ANSIBLE_NET_AUTHORIZE"]), + type="bool", + ), + auth_pass=dict(no_log=True, fallback=(env_fallback, ["ANSIBLE_NET_AUTH_PASS"])), + provider=dict(type="dict", no_log=True), + transport=dict(choices=list()), + timeout=dict(default=10, type="int"), +) + +NET_CONNECTION_ARGS = dict() + +NET_CONNECTIONS = dict() + + +def _transitional_argument_spec(): + argument_spec = {} + for key, value in NET_TRANSPORT_ARGS.items(): + value["required"] = False + argument_spec[key] = value + return argument_spec + + +class ModuleStub(object): + def __init__(self, argument_spec, fail_json): + self.params = dict() + for key, value in argument_spec.items(): + self.params[key] = value.get("default") + self.fail_json = fail_json + + +class NetworkError(Exception): + def __init__(self, msg, **kwargs): + super(NetworkError, self).__init__(msg) + self.kwargs = kwargs + + +class Config(object): + def __init__(self, connection): + self.connection = connection + + def __call__(self, commands, **kwargs): + lines = to_list(commands) + return self.connection.configure(lines, **kwargs) + + def load_config(self, commands, **kwargs): + commands = to_list(commands) + return self.connection.load_config(commands, **kwargs) + + def get_config(self, **kwargs): + return self.connection.get_config(**kwargs) + + def save_config(self): + return self.connection.save_config() + + +class NetworkModule(AnsibleModule): + def __init__(self, *args, **kwargs): + connect_on_load = kwargs.pop("connect_on_load", True) + + argument_spec = NET_TRANSPORT_ARGS.copy() + argument_spec["transport"]["choices"] = NET_CONNECTIONS.keys() + argument_spec.update(NET_CONNECTION_ARGS.copy()) + + if kwargs.get("argument_spec"): + argument_spec.update(kwargs["argument_spec"]) + kwargs["argument_spec"] = argument_spec + + super(NetworkModule, self).__init__(*args, **kwargs) + + self.connection = None + self._cli = None + self._config = None + + try: + transport = self.params["transport"] or "__default__" + cls = NET_CONNECTIONS[transport] + self.connection = cls() + except KeyError: + self.fail_json(msg="Unknown transport or no default transport specified") + except (TypeError, NetworkError) as exc: + self.fail_json(msg=to_native(exc), exception=traceback.format_exc()) + + if connect_on_load: + self.connect() + + @property + def cli(self): + if not self.connected: + self.connect() + if self._cli: + return self._cli + self._cli = Cli(self.connection) + return self._cli + + @property + def config(self): + if not self.connected: + self.connect() + if self._config: + return self._config + self._config = Config(self.connection) + return self._config + + @property + def connected(self): + return self.connection._connected + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get("provider") or dict() + for key, value in provider.items(): + for args in [NET_TRANSPORT_ARGS, NET_CONNECTION_ARGS]: + if key in args: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + try: + if not self.connected: + self.connection.connect(self.params) + if self.params["authorize"]: + self.connection.authorize(self.params) + self.log( + "connected to %s:%s using %s" + % ( + self.params["host"], + self.params["port"], + self.params["transport"], + ) + ) + except NetworkError as exc: + self.fail_json(msg=to_native(exc), exception=traceback.format_exc()) + + def disconnect(self): + try: + if self.connected: + self.connection.disconnect() + self.log("disconnected from %s" % self.params["host"]) + except NetworkError as exc: + self.fail_json(msg=to_native(exc), exception=traceback.format_exc()) + + +def register_transport(transport, default=False): + def register(cls): + NET_CONNECTIONS[transport] = cls + if default: + NET_CONNECTIONS["__default__"] = cls + return cls + + return register + + +def add_argument(key, value): + NET_CONNECTION_ARGS[key] = value + + +def get_resource_connection(module): + if hasattr(module, "_connection"): + return module._connection + + capabilities = get_capabilities(module) + network_api = capabilities.get("network_api") + if network_api == "netconf": + module._connection = NetconfConnection(module._socket_path) + elif network_api == "local": + # not accessible code alert can be taken out at around 01-01-2027, + # when connection local is removed + module._connection = LocalResourceConnection(module) + else: + module._connection = Connection(module._socket_path) + + return module._connection + + +def get_capabilities(module): + if hasattr(module, "capabilities"): + return module._capabilities + try: + capabilities = Connection(module._socket_path).get_capabilities() + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + except AssertionError: + # No socket_path, connection most likely local. + return dict(network_api="local") + module._capabilities = json.loads(capabilities) + + return module._capabilities + + +class LocalResourceConnection: + def __init__(self, module): + self.module = module + + def get(self, *args, **kwargs): + self.module.fail_json(msg="Network resource modules not supported over local connection.") diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py new file mode 100644 index 0000000..26771b6 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py @@ -0,0 +1,284 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c) 2015 Peter Sprygada, +# +# 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 re +import shlex +import time + +from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE +from ansible.module_utils.six.moves import zip + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list + + +text_type = str +string_types = (str,) + + +class FailedConditionsError(Exception): + def __init__(self, msg, failed_conditions): + super(FailedConditionsError, self).__init__(msg) + self.failed_conditions = failed_conditions + + +class FailedConditionalError(Exception): + def __init__(self, msg, failed_conditional): + super(FailedConditionalError, self).__init__(msg) + self.failed_conditional = failed_conditional + + +class AddCommandError(Exception): + def __init__(self, msg, command): + super(AddCommandError, self).__init__(msg) + self.command = command + + +class AddConditionError(Exception): + def __init__(self, msg, condition): + super(AddConditionError, self).__init__(msg) + self.condition = condition + + +class Cli(object): + def __init__(self, connection): + self.connection = connection + self.default_output = connection.default_output or "text" + self._commands = list() + + @property + def commands(self): + return [str(c) for c in self._commands] + + def __call__(self, commands, output=None): + objects = list() + for cmd in to_list(commands): + objects.append(self.to_command(cmd, output)) + return self.connection.run_commands(objects) + + def to_command(self, command, output=None, prompt=None, response=None, **kwargs): + output = output or self.default_output + if isinstance(command, Command): + return command + if isinstance(prompt, string_types): + prompt = re.compile(re.escape(prompt)) + return Command(command, output, prompt=prompt, response=response, **kwargs) + + def add_commands(self, commands, output=None, **kwargs): + for cmd in commands: + self._commands.append(self.to_command(cmd, output, **kwargs)) + + def run_commands(self): + responses = self.connection.run_commands(self._commands) + for resp, cmd in zip(responses, self._commands): + cmd.response = resp + + # wipe out the commands list to avoid issues if additional + # commands are executed later + self._commands = list() + + return responses + + +class Command(object): + def __init__(self, command, output=None, prompt=None, response=None, **kwargs): + self.command = command + self.output = output + self.command_string = command + + self.prompt = prompt + self.response = response + + self.args = kwargs + + def __str__(self): + return self.command_string + + +class CommandRunner(object): + def __init__(self, module): + self.module = module + + self.items = list() + self.conditionals = set() + + self.commands = list() + + self.retries = 10 + self.interval = 1 + + self.match = "all" + + self._default_output = module.connection.default_output + + def add_command(self, command, output=None, prompt=None, response=None, **kwargs): + if command in [str(c) for c in self.commands]: + raise AddCommandError("duplicated command detected", command=command) + cmd = self.module.cli.to_command( + command, output=output, prompt=prompt, response=response, **kwargs + ) + self.commands.append(cmd) + + def get_command(self, command, output=None): + for cmd in self.commands: + if cmd.command == command: + return cmd.response + raise ValueError("command '%s' not found" % command) + + def get_responses(self): + return [cmd.response for cmd in self.commands] + + def add_conditional(self, condition): + try: + self.conditionals.add(Conditional(condition)) + except AttributeError as exc: + raise AddConditionError(msg=str(exc), condition=condition) + + def run(self): + while self.retries > 0: + self.module.cli.add_commands(self.commands) + responses = self.module.cli.run_commands() + + for item in list(self.conditionals): + if item(responses): + if self.match == "any": + return item + self.conditionals.remove(item) + + if not self.conditionals: + break + + time.sleep(self.interval) + self.retries -= 1 + else: + failed_conditions = [item.raw for item in self.conditionals] + errmsg = "One or more conditional statements have not been satisfied" + raise FailedConditionsError(errmsg, failed_conditions) + + +class Conditional(object): + """Used in command modules to evaluate waitfor conditions""" + + OPERATORS = { + "eq": ["eq", "=="], + "neq": ["neq", "ne", "!="], + "gt": ["gt", ">"], + "ge": ["ge", ">="], + "lt": ["lt", "<"], + "le": ["le", "<="], + "contains": ["contains"], + "matches": ["matches"], + } + + def __init__(self, conditional, encoding=None): + self.raw = conditional + self.negate = False + try: + components = shlex.split(conditional) + key, val = components[0], components[-1] + op_components = components[1:-1] + if "not" in op_components: + self.negate = True + op_components.pop(op_components.index("not")) + op = op_components[0] + + except ValueError: + raise ValueError("failed to parse conditional") + + self.key = key + self.func = self._func(op) + self.value = self._cast_value(val) + + def __call__(self, data): + value = self.get_value(dict(result=data)) + if not self.negate: + return self.func(value) + else: + return not self.func(value) + + def _cast_value(self, value): + if value in BOOLEANS_TRUE: + return True + elif value in BOOLEANS_FALSE: + return False + elif re.match(r"^\d+\.d+$", value): + return float(value) + elif re.match(r"^\d+$", value): + return int(value) + else: + return text_type(value) + + def _func(self, oper): + for func, operators in self.OPERATORS.items(): + if oper in operators: + return getattr(self, func) + raise AttributeError("unknown operator: %s" % oper) + + def get_value(self, result): + try: + return self.get_json(result) + except (IndexError, TypeError, AttributeError): + msg = "unable to apply conditional to result" + raise FailedConditionalError(msg, self.raw) + + def get_json(self, result): + string = re.sub(r"\[[\'|\"]", ".", self.key) + string = re.sub(r"[\'|\"]\]", ".", string) + parts = re.split(r"\.(?=[^\]]*(?:\[|$))", string) + for part in parts: + match = re.findall(r"\[(\S+?)\]", part) + if match: + key = part[: part.find("[")] + result = result[key] + for m in match: + try: + m = int(m) + except ValueError: + m = str(m) + result = result[m] + else: + result = result.get(part) + return result + + def number(self, value): + if "." in str(value): + return float(value) + else: + return int(value) + + def eq(self, value): + return value == self.value + + def neq(self, value): + return value != self.value + + def gt(self, value): + return self.number(value) > self.value + + def ge(self, value): + return self.number(value) >= self.value + + def lt(self, value): + return self.number(value) < self.value + + def le(self, value): + return self.number(value) <= self.value + + def contains(self, value): + return str(self.value) in value + + def matches(self, value): + match = re.search(self.value, value, re.M) + return match is not None diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/network_template.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/network_template.py new file mode 100644 index 0000000..1ff9770 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/network_template.py @@ -0,0 +1,134 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2021 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 re + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base import ( + RmEngineBase, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + JinjaTemplate, + dict_merge, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + validate_config as _validate_config, +) + + +try: + from ansible.module_utils.common.parameters import _list_no_log_values as list_no_log_values +except ImportError: + # TODO: Remove this import when we no longer support ansible < 2.11 + from ansible.module_utils.common.parameters import list_no_log_values + + +class NetworkTemplate(RmEngineBase): + """The NetworkTemplate class that Resource Module templates + inherit and use to parse and render config lines. + """ + + def __init__(self, lines=None, tmplt=None, prefix=None, module=None): + super(NetworkTemplate, self).__init__(module=module) + self._lines = lines or [] + self._tmplt = tmplt + self._template = JinjaTemplate() + self._prefix = prefix or {} + + def _deepformat(self, tmplt, data): + wtmplt = deepcopy(tmplt) + if isinstance(tmplt, str): + res = self._template(value=tmplt, variables=data, fail_on_undefined=False) + return res + if isinstance(tmplt, dict): + for tkey, tval in tmplt.items(): + ftkey = self._template(tkey, data) + if ftkey != tkey: + wtmplt.pop(tkey) + if isinstance(tval, dict): + wtmplt[ftkey] = self._deepformat(tval, data) + elif isinstance(tval, list): + wtmplt[ftkey] = [self._deepformat(x, data) for x in tval] + elif isinstance(tval, str): + wtmplt[ftkey] = self._deepformat(tval, data) + if wtmplt[ftkey] is None: + wtmplt.pop(ftkey) + return wtmplt + + def parse(self): + """parse""" + result = {} + shared = {} + for line in self._lines: + for parser in self._tmplt.PARSERS: + cap = re.match(parser["getval"], line) + if cap: + capdict = cap.groupdict() + capdict = dict((k, v) for k, v in capdict.items() if v is not None) + if parser.get("shared"): + shared = capdict + vals = dict_merge(capdict, shared) + res = self._deepformat(deepcopy(parser["result"]), vals) + result = dict_merge(result, res) + break + return result + + def get_parser(self, name): + """get_parsers""" + res = [p for p in self._tmplt.PARSERS if p["name"] == name] + return res[0] + + def _render(self, tmplt, data, negate): + try: + if callable(tmplt): + res = tmplt(data) + else: + res = self._template(value=tmplt, variables=data, fail_on_undefined=False) + except KeyError: + return None + + if res: + if negate: + rem = "{0} ".format(self._prefix.get("remove", "no")) + if isinstance(res, list): + cmd = [(rem + each) for each in res] + return cmd + return rem + res + elif self._prefix.get("set"): + set_cmd = "{0} ".format(self._prefix.get("set", "")) + if isinstance(res, list): + cmd = [(set_cmd + each) for each in res] + return cmd + return set_cmd + res + return res + + def render(self, data, parser_name, negate=False): + """render""" + if negate: + tmplt = ( + self.get_parser(parser_name).get("remval") or self.get_parser(parser_name)["setval"] + ) + else: + tmplt = self.get_parser(parser_name)["setval"] + command = self._render(tmplt, data, negate) + return command + + def validate_config(self, spec, data, redact=False): + validated_data = _validate_config(spec, data) + if redact: + self._module.no_log_values.update(list_no_log_values(spec, validated_data)) + return validated_data diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/resource_module.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/resource_module.py new file mode 100644 index 0000000..7fd97c1 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/resource_module.py @@ -0,0 +1,153 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2021 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 + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base import ( + RmEngineBase, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + get_from_dict, + remove_empties, + to_list, +) + + +class ResourceModule(RmEngineBase): # pylint: disable=R0902 + """Base class for Network Resource Modules""" + + def __init__(self, *_args, **kwargs): + super(ResourceModule, self).__init__(*_args, **kwargs) + self._empty_fact_val = kwargs.get("empty_fact_val", []) + self._facts_module = kwargs.get("facts_module", None) + self._gather_subset = kwargs.get("gather_subset", ["!all", "!min"]) + self._module = kwargs.get("module", None) + self._resource = kwargs.get("resource", None) + self._tmplt = kwargs.get("tmplt", None) + + self.want = remove_empties(self._module.params).get("config", self._empty_fact_val) + # Error out if empty config is passed for following states + if self.state in ("overridden", "merged", "replaced", "rendered") and not self.want: + self._module.fail_json( + msg="value of config parameter must not be empty for state {0}".format(self.state) + ) + + self.before = self.gather_current() + self.have = deepcopy(self.before) + self.changed = False + self.commands = [] + self.warnings = [] + + def gather_current(self): + data = None + if self.state == "rendered": + return self._empty_fact_val + elif self.state == "parsed": + data = self._module.params["running_config"] + if not data: + self._module.fail_json( + msg="value of running_config parameter must not be empty for state parsed" + ) + return deepcopy(self.get_facts(self._empty_fact_val, data=data)) + + @property + def result(self): + """Compute the final result""" + result = {"warnings": self.warnings} + if self.state not in self.ACTION_STATES: + if self.state == "gathered": + result["gathered"] = self.before + elif self.state == "parsed": + result["parsed"] = self.before + elif self.state == "rendered": + result["rendered"] = self.commands + else: + result["commands"] = self.commands + result["before"] = self.before + if self.commands: + result["after"] = self.get_facts(self._empty_fact_val) + result["changed"] = self.changed + return result + + def addcmd(self, data, tmplt, negate=False): + """addcmd""" + command = self._tmplt.render(data, tmplt, negate) + if command: + self.commands.extend(to_list(command)) + + def addcmd_first_found(self, data, tmplts, negate=False): + """addcmd first found""" + for pname in tmplts: + before = len(self.commands) + self.addcmd(data, pname, negate) + if len(self.commands) != before: + break + + def get_facts(self, empty_val=None, data=None): + """Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + if empty_val is None: + empty_val = [] + facts, _warnings = self._facts_module.get_facts( + legacy_facts_type=self._gather_subset, + resource_facts_type=[self._resource], + data=data, + ) + facts = facts["ansible_network_resources"].get(self._resource) + if not facts: + return empty_val + return facts + + def compare(self, parsers, want=None, have=None): + """Run through all the parsers and compare + the want and have dicts + """ + if want is None: + want = self.want + if have is None: + have = self.have + for parser in to_list(parsers): + compval = self._tmplt.get_parser(parser).get("compval") + if not compval: + compval = parser + inw = get_from_dict(want, compval) + inh = get_from_dict(have, compval) + + if isinstance(inw, dict) and inw.get("set") is False and not inh: + continue + + if inw is not None and inw != inh: + if isinstance(inw, bool): + if inw is False and inh is None: + continue + self.addcmd(want, parser, not inw) + else: + self.addcmd(want, parser, False) + elif inw is None and inh is not None: + if isinstance(inh, bool): + self.addcmd(have, parser, inh) + else: + self.addcmd(have, parser, True) + + def run_commands(self): + """Send commands to the device""" + if self.commands and self.state in self.ACTION_STATES: + if not self._module.check_mode: + self._connection.edit_config(candidate=self.commands) + self.changed = True diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/resource_module_base.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/resource_module_base.py new file mode 100644 index 0000000..bb002a1 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/rm_base/resource_module_base.py @@ -0,0 +1,45 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2021 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 + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network import ( + get_resource_connection, +) + + +class RmEngineBase(object): # pylint: disable=R0902 + """Base class for Network Resource Modules""" + + ACTION_STATES = ["merged", "replaced", "overridden", "deleted", "purged"] + + def __init__(self, *_args, **kwargs): + self._connection = None + self._module = kwargs.get("module", None) + self.state = None + # backward compatibility for modules, in which, module is not passed + # to the NetworkTemplate + if self._module: + if "state" in self._module.params.keys(): + self.state = self._module.params["state"] + + self._get_connection() + + def _get_connection(self): + if self.state not in ["rendered", "parsed"]: + if self._connection: + return self._connection + self._connection = get_resource_connection(self._module) + return self._connection + return None diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py new file mode 100644 index 0000000..8cef923 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py @@ -0,0 +1,749 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2016 Red Hat Inc. +# +# 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 + +# Networking tools for network modules only + +import ast +import operator +import re +import socket + +from copy import deepcopy +from functools import reduce # forward compatibility for Python 3 +from io import StringIO +from itertools import chain + +from ansible.module_utils import basic +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.parsing.convert_bool import boolean + + +try: + from collections.abc import Mapping +except ImportError: + # Python 2.7 fallback for ansible-core 2.16: + from ansible.module_utils.common._collections_compat import Mapping + + +string_types = (str,) +try: + from jinja2 import Environment, StrictUndefined + from jinja2.exceptions import UndefinedError + + HAS_JINJA2 = True +except ImportError: + HAS_JINJA2 = False + +try: + import yaml + + try: + # use C version if possible for speedup + from yaml import CSafeLoader as SafeLoader + except ImportError: + from yaml import SafeLoader + HAS_YAML = True +except ImportError: + HAS_YAML = False + +OPERATORS = frozenset(["ge", "gt", "eq", "neq", "lt", "le"]) +ALIASES = frozenset([("min", "ge"), ("max", "le"), ("exactly", "eq"), ("neq", "ne")]) + +OPTION_METADATA = ( + "type", + "choices", + "default", + "required", + "aliases", + "elements", + "fallback", + "no_log", + "apply_defaults", + "deprecated_aliases", + "removed_in_version", +) +OPTION_CONDITIONALS = ( + "mutually_exclusive", + "required_one_of", + "required_together", + "required_by", + "required_if", +) + +VALID_ANSIBLEMODULE_ARGS = ( + "bypass_checks", + "no_log", + "add_file_common_args", + "supports_check_mode", +) + OPTION_CONDITIONALS + + +def to_list(val): + if isinstance(val, (list, tuple, set)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = to_text(item).split("\n") + yield item + + +def transform_commands(module): + transform = EntityCollection( + module, + dict( + command=dict(key=True), + output=dict(), + prompt=dict(type="list"), + answer=dict(type="list"), + newline=dict(type="bool", default=True), + sendonly=dict(type="bool", default=False), + check_all=dict(type="bool", default=False), + ), + ) + + return transform(module.params["commands"]) + + +def sort_list(val): + if isinstance(val, list): + if isinstance(val[0], dict): + sorted_keys = [tuple(sorted(dict_.keys())) for dict_ in val] + # All keys should be identical + if len(set(sorted_keys)) != 1: + raise ValueError("dictionaries do not match") + + return sorted(val, key=lambda d: tuple(d[k] for k in sorted_keys[0])) + return sorted(val) + return val + + +class Entity(object): + """Transforms a dict to with an argument spec + + This class will take a dict and apply an Ansible argument spec to the + values. The resulting dict will contain all of the keys in the param + with appropriate values set. + + Example:: + + argument_spec = dict( + command=dict(key=True), + display=dict(default='text', choices=['text', 'json']), + validate=dict(type='bool') + ) + transform = Entity(module, argument_spec) + value = dict(command='foo') + result = transform(value) + print(result) + {'command': 'foo', 'display': 'text', 'validate': None} + + Supported argument spec: + * key - specifies how to map a single value to a dict + * read_from - read and apply the argument_spec from the module + * required - a value is required + * type - type of value (uses AnsibleModule type checker) + * fallback - implements fallback function + * choices - set of valid options + * default - default value + """ + + def __init__(self, module, attrs=None, args=None, keys=None, from_argspec=False): + args = [] if args is None else args + + self._attributes = attrs or {} + self._module = module + + for arg in args: + self._attributes[arg] = dict() + if from_argspec: + self._attributes[arg]["read_from"] = arg + if keys and arg in keys: + self._attributes[arg]["key"] = True + + self.attr_names = frozenset(self._attributes.keys()) + + _has_key = False + + for name, attr in self._attributes.items(): + if attr.get("read_from"): + if attr["read_from"] not in self._module.argument_spec: + module.fail_json(msg="argument %s does not exist" % attr["read_from"]) + spec = self._module.argument_spec.get(attr["read_from"]) + for key, value in spec.items(): + if key not in attr: + attr[key] = value + + if attr.get("key"): + if _has_key: + module.fail_json(msg="only one key value can be specified") + _has_key = True + attr["required"] = True + + def serialize(self): + return self._attributes + + def to_dict(self, value): + obj = {} + for name, attr in self._attributes.items(): + if attr.get("key"): + obj[name] = value + else: + obj[name] = attr.get("default") + return obj + + def __call__(self, value, strict=True): + if not isinstance(value, dict): + value = self.to_dict(value) + + if strict: + unknown = set(value).difference(self.attr_names) + if unknown: + self._module.fail_json(msg="invalid keys: %s" % ",".join(unknown)) + + for name, attr in self._attributes.items(): + if value.get(name) is None: + value[name] = attr.get("default") + + if attr.get("fallback") and not value.get(name): + fallback = attr.get("fallback", (None,)) + fallback_strategy = fallback[0] + fallback_args = [] + fallback_kwargs = {} + if fallback_strategy is not None: + for item in fallback[1:]: + if isinstance(item, dict): + fallback_kwargs = item + else: + fallback_args = item + try: + value[name] = fallback_strategy(*fallback_args, **fallback_kwargs) + except basic.AnsibleFallbackNotFound: + continue + + if attr.get("required") and value.get(name) is None: + self._module.fail_json(msg="missing required attribute %s" % name) + + if "choices" in attr: + if value[name] not in attr["choices"]: + self._module.fail_json( + msg="%s must be one of %s, got %s" + % (name, ", ".join(attr["choices"]), value[name]) + ) + + if value[name] is not None: + value_type = attr.get("type", "str") + type_checker = self._module._CHECK_ARGUMENT_TYPES_DISPATCHER[value_type] + type_checker(value[name]) + elif value.get(name): + value[name] = self._module.params[name] + + return value + + +class EntityCollection(Entity): + """Extends ```Entity``` to handle a list of dicts""" + + def __call__(self, iterable, strict=True): + if iterable is None: + iterable = [super(EntityCollection, self).__call__(self._module.params, strict)] + + if not isinstance(iterable, (list, tuple)): + self._module.fail_json(msg="value must be an iterable") + + return [(super(EntityCollection, self).__call__(i, strict)) for i in iterable] + + +class ComplexList(EntityCollection): + """Alternate name for EntityCollection for backwards compatibility""" + + def __init__(self, attrs, module, *args, **kwargs): + super(ComplexList, self).__init__(module, attrs, *args, **kwargs) + + +def dict_diff(base, comparable): + """Generate a dict object of differences + + This function will compare two dict objects and return the difference + between them as a dict object. For scalar values, the key will reflect + the updated value. If the key does not exist in `comparable`, then then no + key will be returned. For lists, the value in comparable will wholly replace + the value in base for the key. For dicts, the returned value will only + return keys that are different. + + :param base: dict object to base the diff on + :param comparable: dict object to compare against base + + :returns: new dict object with differences + """ + if not isinstance(base, dict): + raise AssertionError("`base` must be of type ") + if not isinstance(comparable, dict): + if comparable is None: + comparable = dict() + else: + raise AssertionError("`comparable` must be of type ") + + updates = dict() + + for key, value in base.items(): + if isinstance(value, dict): + item = comparable.get(key) + if item is not None: + sub_diff = dict_diff(value, comparable[key]) + if sub_diff: + updates[key] = sub_diff + else: + comparable_value = comparable.get(key) + if comparable_value is not None: + if sort_list(base[key]) != sort_list(comparable_value): + updates[key] = comparable_value + + for key in set(comparable.keys()).difference(base.keys()): + updates[key] = comparable.get(key) + + return updates + + +def dict_merge(base, other): + """Return a new dict object that combines base and other + + This will create a new dict object that is a combination of the key/value + pairs from base and other. When both keys exist, the value will be + selected from other. If the value is a list object, the two lists will + be combined and duplicate entries removed. + + :param base: dict object to serve as base + :param other: dict object to combine with base + + :returns: new combined dict object + """ + if not isinstance(base, dict): + raise AssertionError("`base` must be of type ") + if not isinstance(other, dict): + raise AssertionError("`other` must be of type ") + + combined = dict() + + for key, value in deepcopy(base).items(): + if isinstance(value, dict): + if key in other: + item = other.get(key) + if item is not None: + if isinstance(other[key], Mapping): + combined[key] = dict_merge(value, other[key]) + else: + combined[key] = other[key] + else: + combined[key] = item + else: + combined[key] = value + elif isinstance(value, list): + if key in other: + item = other.get(key) + if item is not None: + try: + combined[key] = list(set(chain(value, item))) + except TypeError: + value.extend([i for i in item if i not in value]) + combined[key] = value + else: + combined[key] = item + else: + combined[key] = value + else: + if key in other: + other_value = other.get(key) + if other_value is not None: + if sort_list(base[key]) != sort_list(other_value): + combined[key] = other_value + else: + combined[key] = value + else: + combined[key] = other_value + else: + combined[key] = value + + for key in set(other.keys()).difference(base.keys()): + combined[key] = other.get(key) + + return combined + + +def param_list_to_dict(param_list, unique_key="name", remove_key=True): + """Rotates a list of dictionaries to be a dictionary of dictionaries. + + :param param_list: The aforementioned list of dictionaries + :param unique_key: The name of a key which is present and unique in all of param_list's dictionaries. The value + behind this key will be the key each dictionary can be found at in the new root dictionary + :param remove_key: If True, remove unique_key from the individual dictionaries before returning. + """ + param_dict = {} + for params in param_list: + params = params.copy() + if remove_key: + name = params.pop(unique_key) + else: + name = params.get(unique_key) + param_dict[name] = params + + return param_dict + + +def conditional(expr, val, cast=None): + match = re.match(r"^(.+)\((.+)\)$", str(expr), re.I) + if match: + op, arg = match.groups() + else: + op = "eq" + if " " in str(expr): + raise AssertionError("invalid expression: cannot contain spaces") + arg = expr + + if cast is None and val is not None: + arg = type(val)(arg) + elif callable(cast): + arg = cast(arg) + val = cast(val) + + op = next((oper for alias, oper in ALIASES if op == alias), op) + + if not hasattr(operator, op) and op not in OPERATORS: + raise ValueError("unknown operator: %s" % op) + + func = getattr(operator, op) + return func(val, arg) + + +def ternary(value, true_val, false_val): + """value ? true_val : false_val""" + if value: + return true_val + else: + return false_val + + +def remove_default_spec(spec): + for item in spec: + if "default" in spec[item]: + del spec[item]["default"] + + +def validate_ip_address(address): + try: + socket.inet_aton(address) + except socket.error: + return False + return address.count(".") == 3 + + +def validate_ip_v6_address(address): + try: + socket.inet_pton(socket.AF_INET6, address) + except socket.error: + return False + return True + + +def validate_prefix(prefix): + if prefix and not 0 <= int(prefix) <= 32: + return False + return True + + +def load_provider(spec, args): + provider = args.get("provider") or {} + for key, value in spec.items(): + if key not in provider: + try: + # Get fallback if defined, and valid + provider[key] = _fallback(value["fallback"]) + except (basic.AnsibleFallbackNotFound, KeyError): + # Get default if defined, otherwise set to None + provider[key] = value.get("default") + if "authorize" in provider: + # Coerce authorize to provider if a string has somehow snuck in. + provider["authorize"] = boolean(provider["authorize"] or False) + args["provider"] = provider + return provider + + +def _fallback(fallback): + strategy = fallback[0] + args = [] + kwargs = {} + + for item in fallback[1:]: + if isinstance(item, dict): + kwargs = item + else: + args = item + + return strategy(*args, **kwargs) + + +def generate_dict(spec): + """ + Generate dictionary which is in sync with argspec + + :param spec: A dictionary that is the argspec of the module + :rtype: A dictionary + :returns: A dictionary in sync with argspec with default value + """ + obj = {} + if not spec: + return obj + + for key, val in spec.items(): + if "default" in val: + dct = {key: val["default"]} + elif "type" in val and val["type"] == "dict": + dct = {key: generate_dict(val["options"])} + else: + dct = {key: None} + obj.update(dct) + return obj + + +def parse_conf_arg(cfg, arg): + """ + Parse config based on argument + + :param cfg: A text string which is a line of configuration. + :param arg: A text string which is to be matched. + :rtype: A text string + :returns: A text string if match is found + """ + match = re.search(r"%s (.+)(\n|$)" % arg, cfg, re.M) + if match: + result = match.group(1).strip() + else: + result = None + return result + + +def parse_conf_cmd_arg(cfg, cmd, res1, res2=None, delete_str="no"): + """ + Parse config based on command + + :param cfg: A text string which is a line of configuration. + :param cmd: A text string which is the command to be matched + :param res1: A text string to be returned if the command is present + :param res2: A text string to be returned if the negate command + is present + :param delete_str: A text string to identify the start of the + negate command + :rtype: A text string + :returns: A text string if match is found + """ + match = re.search(r"\n\s+%s(\n|$)" % cmd, cfg) + if match: + return res1 + if res2 is not None: + match = re.search(r"\n\s+%s %s(\n|$)" % (delete_str, cmd), cfg) + if match: + return res2 + return None + + +def get_xml_conf_arg(cfg, path, data="text"): + """ + :param cfg: The top level configuration lxml Element tree object + :param path: The relative xpath w.r.t to top level element (cfg) + to be searched in the xml hierarchy + :param data: The type of data to be returned for the matched xml node. + Valid values are text, tag, attrib, with default as text. + :return: Returns the required type for the matched xml node or else None + """ + match = cfg.xpath(path) + if len(match): + if data == "tag": + result = getattr(match[0], "tag") + elif data == "attrib": + result = getattr(match[0], "attrib") + else: + result = getattr(match[0], "text") + else: + result = None + return result + + +def remove_empties(cfg_dict): + """ + Generate final config dictionary + + :param cfg_dict: A dictionary parsed in the facts system + :rtype: A dictionary + :returns: A dictionary by eliminating keys that have null values + """ + final_cfg = {} + if not cfg_dict: + return final_cfg + + for key, val in cfg_dict.items(): + dct = None + if isinstance(val, dict): + child_val = remove_empties(val) + if child_val: + dct = {key: child_val} + elif isinstance(val, list) and val and all(isinstance(x, dict) for x in val): + child_val = [remove_empties(x) for x in val] + if child_val: + dct = {key: child_val} + elif val not in [None, [], {}, (), ""]: + dct = {key: val} + if dct: + final_cfg.update(dct) + return final_cfg + + +def validate_config(spec, data): + """ + Validate if the input data against the AnsibleModule spec format + :param spec: Ansible argument spec + :param data: Data to be validated + :return: + """ + + class DirectValidationModule(basic.AnsibleModule): + def _load_params(self): + self.params = deepcopy(data) + + validated_data = DirectValidationModule(spec).params + + return validated_data + + +def search_obj_in_list(name, lst, key="name"): + if not lst: + return None + else: + for item in lst: + if item.get(key) == name: + return item + + +def get_from_dict(data_dict, keypath): + """get from dictionary""" + map_list = keypath.split(".") + try: + return reduce(operator.getitem, map_list, data_dict) + except KeyError: + return None + + +def compare_partial_dict(want, have, compare_keys): + """compare""" + rmkeys = [ckey[1:] for ckey in compare_keys if ckey.startswith("!")] + kkeys = [ckey for ckey in compare_keys if not ckey.startswith("!")] + + wantd = {} + for key, val in want.items(): + if key not in rmkeys or key in kkeys: + wantd[key] = val + + haved = {} + for key, val in have.items(): + if key not in rmkeys or key in kkeys: + haved[key] = val + + return wantd == haved + + +class JinjaTemplate: + """Custom Jinja2-based template class for network module utilities. + + Note: This class was renamed from 'Template' to 'JinjaTemplate' to avoid + naming collision with Jinja2's Template class in Ansible 2.19+. + """ + + def __init__(self): + if not HAS_JINJA2: + raise ImportError( + "jinja2 is required but does not appear to be installed. " + "It can be installed using `pip install jinja2`" + ) + + self.env = Environment(undefined=StrictUndefined) + self.env.filters.update({"ternary": ternary, "all": all, "any": any}) + + def __call__(self, value, variables=None, fail_on_undefined=True): + variables = variables or {} + + if not self.contains_vars(value): + return value + + try: + value = self.env.from_string(value).render(variables) + except UndefinedError: + if not fail_on_undefined: + return None + raise + + if value: + try: + return ast.literal_eval(value) + except Exception: + return str(value) + else: + return None + + def contains_vars(self, data): + if isinstance(data, string_types): + for marker in ( + self.env.block_start_string, + self.env.variable_start_string, + self.env.comment_start_string, + ): + if marker in data: + return True + return False + + +# Backwards compatibility alias for code that imports 'Template' from here +Template = JinjaTemplate + + +def extract_argspec(doc_obj, argpsec): + options_obj = doc_obj.get("options") + for okey, ovalue in options_obj.items(): + argpsec[okey] = {} + for metakey in list(ovalue): + if metakey == "suboptions": + argpsec[okey].update({"options": {}}) + suboptions_obj = {"options": ovalue["suboptions"]} + extract_argspec(suboptions_obj, argpsec[okey]["options"]) + elif metakey in OPTION_METADATA + OPTION_CONDITIONALS: + argpsec[okey].update({metakey: ovalue[metakey]}) + + +# TODO: Support extends_documentation_fragment +def convert_doc_to_ansible_module_kwargs(doc): + doc_obj = yaml.load(StringIO(doc), SafeLoader) + argspec = {} + spec = {} + extract_argspec(doc_obj, argspec) + spec.update({"argument_spec": argspec}) + for item in doc_obj: + if item in VALID_ANSIBLEMODULE_ARGS: + spec.update({item: doc_obj[item]}) + return spec diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/grpc/grpc.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/grpc/grpc.py new file mode 100644 index 0000000..8f449bc --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/grpc/grpc.py @@ -0,0 +1,131 @@ +# (c) 2022 Red Hat, Inc. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import re + +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import Connection + + +def get_connection(module): + if hasattr(module, "_grpc_connection"): + return module._grpc_connection + + capabilities = get_capabilities(module) + network_api = capabilities.get("network_api") + if network_api == "ansible.netcommon.grpc": + module._grpc_connection = Connection(module._socket_path) + else: + module.fail_json(msg="Invalid connection type %s" % network_api) + return module._grpc_connection + + +def get_capabilities(module): + if hasattr(module, "_grpc_capabilities"): + return module._grpc_capabilities + + module._grpc_capabilities = Connection(module._socket_path).get_capabilities() + return module._grpc_capabilities + + +def get(module, section, data_type, check_rc=True): + conn = get_connection(module) + if data_type == "config": + out = conn.get_config(section) + else: + out = conn.get(section) + + response = out.get("response") + error = out.get("error") + if error: + if check_rc: + module.fail_json(msg=to_text(out["error"], errors="surrogate_then_replace")) + else: + module.warn(to_text(out["error"], errors="surrogate_then_replace")) + + return response.strip(), error.strip() + + +def merge_config(module, section, check_rc=True): + conn = get_connection(module) + try: + out = conn.merge_config(section) + if out: + err = json.loads(out) + res = json.dumps(err, indent=4, separators=(",", ": ")) + module.fail_json(msg=to_text(res, errors="surrogate_then_replace")) + except ValueError as err: + if check_rc: + module.fail_json(msg=to_text(out["error"], errors="surrogate_then_replace")) + else: + module.warn(to_text(out["error"], errors="surrogate_then_replace")) + return err + + +def replace_config(module, section, check_rc=True): + conn = get_connection(module) + try: + out = conn.replace_config(section) + if out: + err = json.loads(out) + res = json.dumps(err, indent=4, separators=(",", ": ")) + module.fail_json(msg=to_text(res, errors="surrogate_then_replace")) + except ValueError as err: + if check_rc: + module.fail_json(msg=to_text(out["error"], errors="surrogate_then_replace")) + else: + module.warn(to_text(out["error"], errors="surrogate_then_replace")) + return err + + +def delete_config(module, section, check_rc=True): + conn = get_connection(module) + try: + out = conn.delete_config(section) + if out: + err = json.loads(out) + res = json.dumps(err, indent=4, separators=(",", ": ")) + module.fail_json(msg=to_text(res, errors="surrogate_then_replace")) + except ValueError as err: + if check_rc: + module.fail_json(msg=to_text(out["error"], errors="surrogate_then_replace")) + else: + module.warn(to_text(out["error"], errors="surrogate_then_replace")) + return err + + +def run_cli(module, command, display, check_rc=True): + conn = get_connection(module) + out = conn.run_cli(command, display) + response = out.get("response") + error = out.get("error") + if error: + if check_rc: + module.fail_json(msg=to_text(out["error"], errors="surrogate_then_replace")) + else: + module.warn(to_text(out["error"], errors="surrogate_then_replace")) + + return response.strip(), error.strip() + + +def sanitize_content(data): + out = re.sub(".*Last configuration change.*\n?", "", data) + return out + + +def validate_config(module, config): + output = "" + params = list(config.keys())[0] + if params: + val = "{" + '"' + params + '": [null]}' + response, err = get(module, val, "config") + if response: + output = json.loads(response) + return output diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/netconf/netconf.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/netconf/netconf.py new file mode 100644 index 0000000..9a784ae --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/netconf/netconf.py @@ -0,0 +1,134 @@ +# +# (c) 2018 Red Hat, Inc. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type +import json + +from contextlib import contextmanager +from copy import deepcopy + + +try: + from lxml.etree import fromstring, tostring +except ImportError: + from xml.etree.ElementTree import fromstring, tostring + +from ansible.module_utils.common.text.converters import to_bytes, to_text +from ansible.module_utils.connection import Connection, ConnectionError + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import ( + NetconfConnection, +) + + +IGNORE_XML_ATTRIBUTE = () + + +def get_connection(module): + if hasattr(module, "_netconf_connection"): + return module._netconf_connection + + capabilities = get_capabilities(module) + network_api = capabilities.get("network_api") + if network_api == "netconf": + module._netconf_connection = NetconfConnection(module._socket_path) + else: + module.fail_json(msg="Invalid connection type %s" % network_api) + + return module._netconf_connection + + +def get_capabilities(module): + if hasattr(module, "_netconf_capabilities"): + return module._netconf_capabilities + + capabilities = Connection(module._socket_path).get_capabilities() + module._netconf_capabilities = json.loads(capabilities) + return module._netconf_capabilities + + +def lock_configuration(module, target=None): + conn = get_connection(module) + return conn.lock(target=target) + + +def unlock_configuration(module, target=None): + conn = get_connection(module) + return conn.unlock(target=target) + + +@contextmanager +def locked_config(module, target=None): + try: + lock_configuration(module, target=target) + yield + finally: + unlock_configuration(module, target=target) + + +def get_config(module, source, filter=None, lock=False): + conn = get_connection(module) + locked = False + + try: + if lock: + conn.lock(target=source) + locked = True + response = conn.get_config(source=source, filter=filter) + + except ConnectionError as e: + module.fail_json(msg=to_text(e, errors="surrogate_then_replace").strip()) + + finally: + if locked: + conn.unlock(target=source) + + return response + + +def get(module, filter, lock=False): + conn = get_connection(module) + locked = False + + try: + if lock: + conn.lock(target="running") + locked = True + + response = conn.get(filter=filter) + + except ConnectionError as e: + module.fail_json(msg=to_text(e, errors="surrogate_then_replace").strip()) + + finally: + if locked: + conn.unlock(target="running") + + return response + + +def dispatch(module, request): + conn = get_connection(module) + try: + response = conn.dispatch(request) + except ConnectionError as e: + module.fail_json(msg=to_text(e, errors="surrogate_then_replace").strip()) + + return response + + +def sanitize_xml(data): + tree = fromstring(to_bytes(deepcopy(data), errors="surrogate_then_replace")) + for element in tree.iter(): + # remove attributes + attribute = element.attrib + if attribute: + for key in list(attribute): + if key not in IGNORE_XML_ATTRIBUTE: + attribute.pop(key) + return to_text(tostring(tree), errors="surrogate_then_replace").strip() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/restconf/restconf.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/restconf/restconf.py new file mode 100644 index 0000000..3f14fa9 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/restconf/restconf.py @@ -0,0 +1,45 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2018 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 + +from ansible.module_utils.connection import Connection + + +def get(module, path=None, content=None, fields=None, output="json"): + if path is None: + raise ValueError("path value must be provided") + if content: + path += "?" + "content=%s" % content + if fields: + path += "?" + "field=%s" % fields + + accept = None + if output == "xml": + accept = "application/yang-data+xml" + + connection = Connection(module._socket_path) + return connection.send_request(None, path=path, method="GET", accept=accept) + + +def edit_config(module, path=None, content=None, method="GET", format="json"): + if path is None: + raise ValueError("path value must be provided") + + content_type = None + if format == "xml": + content_type = "application/yang-data+xml" + + connection = Connection(module._socket_path) + return connection.send_request(content, path=path, method=method, content_type=content_type) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/module_utils/utils/data.py b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/utils/data.py new file mode 100644 index 0000000..529bd97 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/module_utils/utils/data.py @@ -0,0 +1,198 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# 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 +""" +Utils functions for handle data formatting +""" +import json +import sys + +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native + + +string_types = (str,) +try: + HAS_LXML = True + from lxml import etree + from lxml.etree import XMLSyntaxError, fromstring + +except ImportError: + HAS_LXML = False + from xml.etree.ElementTree import fromstring + + if sys.version_info < (2, 7): + from xml.parsers.expat import ExpatError as XMLSyntaxError + else: + from xml.etree.ElementTree import ParseError as XMLSyntaxError + + +try: + import xmltodict + + HAS_XMLTODICT = True +except ImportError: + HAS_XMLTODICT = False + + +def validate_and_normalize_data(data, fmt=None): + """ + This function validates the data for given format (fmt). + If the fmt is None it tires to guess the data format. + Currently support data format checks are + 1) xml + 2) json + 3) xpath + :param data: The data which should be validated and normalised. + :param fmt: This is an optional argument which indicated the format + of the data. Valid values are "xml", "json" and "xpath". If the value + is None the format of the data will be guessed and returned in the output. + :return: + * If the format identified is XML it parses the xml data and returns + a tuple of lxml.etree.Element class object and the data format type + which is "xml" in this case. + + * If the format identified is JSON it parses the json data and returns + a tuple of dict object and the data format type + which is "json" in this case. + + * If the format identified is XPATH it parses the XPATH data and returns + a tuple of etree.XPath class object and the data format type + which is "xpath" in this case. For this type lxml library is required + to be installed. + """ + if data is None: + return None, None + + if isinstance(data, string_types): + data = data.strip() + if (data.startswith("<") and data.endswith(">")) or fmt == "xml": + try: + result = fromstring(data) + if fmt and fmt != "xml": + raise Exception( + "Invalid format '%s'. Expected format is 'xml' for data '%s'" % (fmt, data) + ) + return result, "xml" + except XMLSyntaxError as exc: + if fmt == "xml": + raise Exception( + "'%s' XML validation failed with error '%s'" + % ( + data, + to_native(exc, errors="surrogate_then_replace"), + ) + ) + pass + except Exception as exc: + error = "'%s' recognized as XML but was not valid." % data + raise Exception(error + to_native(exc, errors="surrogate_then_replace")) + elif (data.startswith("{") and data.endswith("}")) or fmt == "json": + try: + result = json.loads(data) + if fmt and fmt != "json": + raise Exception( + "Invalid format '%s'. Expected format is 'json' for data '%s'" % (fmt, data) + ) + return result, "json" + except ( + TypeError, + getattr(json.decoder, "JSONDecodeError", ValueError), + ) as exc: + if fmt == "json": + raise Exception( + "'%s' JSON validation failed with error '%s'" + % ( + data, + to_native(exc, errors="surrogate_then_replace"), + ) + ) + except Exception as exc: + error = "'%s' recognized as JSON but was not valid." % data + raise Exception(error + to_native(exc, errors="surrogate_then_replace")) + else: + try: + if not HAS_LXML: + raise Exception(missing_required_lib("lxml")) + + result = etree.XPath(data) + if fmt and fmt != "xpath": + raise Exception( + "Invalid format '%s'. Expected format is 'xpath' for data '%s'" + % (fmt, data) + ) + return result, "xpath" + except etree.XPathSyntaxError as exc: + if fmt == "xpath": + raise Exception( + "'%s' XPath validation failed with error '%s'" + % ( + data, + to_native(exc, errors="surrogate_then_replace"), + ) + ) + pass + except Exception as exc: + error = "'%s' recognized as Xpath but was not valid." % data + raise Exception(error + to_native(exc, errors="surrogate_then_replace")) + + elif isinstance(data, dict): + if fmt and fmt != "json": + raise Exception( + "Invalid format '%s'. Expected format is 'json' for data '%s'" % (fmt, data) + ) + + try: + result = json.loads(json.dumps(data)) + return result, "json" + except ( + TypeError, + getattr(json.decoder, "JSONDecodeError", ValueError), + ) as exc: + raise Exception( + "'%s' JSON validation failed with error '%s'" + % (data, to_native(exc, errors="surrogate_then_replace")) + ) + except Exception as exc: + error = "'%s' recognized as JSON but was not valid." % data + raise Exception(error + to_native(exc, errors="surrogate_then_replace")) + + return data, None + + +def xml_to_dict(data): + if not HAS_XMLTODICT: + msg = "xml to dict conversion requires 'xmltodict' for given data %s ." % data + raise Exception(msg + missing_required_lib("xmltodict")) + + try: + return xmltodict.parse(data, dict_constructor=dict) + except Exception as exc: + error = "'xmltodict' returned the following error when converting %s to dict. " % data + raise Exception(error + to_native(exc, errors="surrogate_then_replace")) + + +def dict_to_xml(data, full_document=False): + """ + Converts dict object to a valid XML string + :param data: Python dict object + :param full_document: When set to True the will have exactly one root. + :return: Valid XML string + """ + if not HAS_XMLTODICT: + msg = "dict to xml conversion requires 'xmltodict' for given data %s ." % data + raise Exception(msg + missing_required_lib("xmltodict")) + + try: + return xmltodict.unparse(data, full_document=full_document) + except Exception as exc: + error = "'xmltodict' returned the following error when converting %s to xml. " % data + raise Exception(error + to_native(exc, errors="surrogate_then_replace")) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_backup.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_backup.py new file mode 100644 index 0000000..033743b --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_backup.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, Ansible by 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 = """ +module: cli_backup +author: Katherine Case (@Qalthos) +short_description: Back up device configuration from network devices over network_cli +description: +- This module provides platform agnostic way of backing up text based configuration from + network devices over network_cli connection plugin. +version_added: 4.2.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + defaults: + description: + - The I(defaults) argument will influence how the running-config is collected + from the device. When the value is set to true, the command used to collect + the running-config is append with the all keyword. When the value is set to + false, the command is issued without the all keyword. + default: no + type: bool + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given it will be generated based on the hostname, current time and + date in format defined by _config.@ + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist it will + be first created and the filename is either the value of C(filename) or + default filename as described in C(filename) options description. If the + path value is not given in that case a I(backup) directory will be created + in the current working directory and backup configuration will be copied + in C(filename) within I(backup) directory. + type: path +""" + +EXAMPLES = """ +- name: configurable backup path + ansible.netcommon.cli_backup: + filename: backup.cfg + dir_path: /home/user +""" + +RETURN = """ +backup_path: + description: The full path to the backup file + returned: always + type: str + sample: /playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import Connection + + +def validate_args(module, device_operations): + """validate param if it is supported on the platform""" + feature_list = [ + "defaults", + ] + + for feature in feature_list: + if module.params[feature]: + supports_feature = device_operations.get("supports_%s" % feature) + if supports_feature is None: + module.fail_json( + msg="This platform does not specify whether %s is supported or not. " + "Please report an issue against this platform's cliconf plugin." % feature + ) + elif not supports_feature: + module.fail_json(msg="Option %s is not supported on this platform" % feature) + + +def main(): + """main entry point for execution""" + argument_spec = dict( + defaults=dict(default=False, type="bool"), + filename=dict(), + dir_path=dict(type="path"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + ) + + result = {"changed": False} + + connection = Connection(module._socket_path) + capabilities = module.from_json(connection.get_capabilities()) + + if capabilities: + device_operations = capabilities.get("device_operations", dict()) + validate_args(module, device_operations) + else: + device_operations = dict() + + if module.params["defaults"]: + if "get_default_flag" in capabilities.get("rpc"): + flags = connection.get_default_flag() + else: + flags = "all" + else: + flags = [] + + running = connection.get_config(flags=flags) + result["__backup__"] = running + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_command.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_command.py new file mode 100644 index 0000000..d65707f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_command.py @@ -0,0 +1,198 @@ +#!/usr/bin/python +# Copyright: 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: cli_command +author: Katherine Case (@Qalthos) +short_description: Run a cli command on cli-based network devices +description: +- Sends a command to a network device and returns the result read from the device. +version_added: 1.0.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + command: + description: + - The command to send to the remote network device. The resulting output from + the command is returned, unless I(sendonly) is set. + required: true + type: str + prompt: + description: + - A single regex pattern or a sequence of patterns to evaluate the expected prompt + from I(command). + required: false + type: list + elements: str + answer: + description: + - The answer to reply with if I(prompt) is matched. The value can be a single + answer or a list of answer for multiple prompts. In case the command execution + results in multiple prompts the sequence of the prompt and excepted answer should + be in same order. + required: false + type: list + elements: str + sendonly: + description: + - The boolean value, that when set to true will send I(command) to the device + but not wait for a result. + type: bool + default: false + required: false + newline: + description: + - The boolean value, that when set to false will send I(answer) to the device + without a trailing newline. + type: bool + default: true + required: false + check_all: + description: + - By default if any one of the prompts mentioned in C(prompt) option is matched + it won't check for other prompts. This boolean flag, that when set to I(True) + will check for all the prompts mentioned in C(prompt) option in the given order. + If the option is set to I(True) all the prompts should be received from remote + host if not it will result in timeout. + type: bool + default: false +""" + +EXAMPLES = """ +- name: run show version on remote devices + ansible.netcommon.cli_command: + command: show version + +- name: run command with json formatted output + ansible.netcommon.cli_command: + command: show version | json + +- name: run command expecting user confirmation + ansible.netcommon.cli_command: + command: commit replace + prompt: This commit will replace or remove the entire running configuration + answer: "yes" + +- name: run command expecting user confirmation + ansible.netcommon.cli_command: + command: show interface summary + prompt: Press any key to continue + answer: y + newline: false + +- name: run config mode command and handle prompt/answer + ansible.netcommon.cli_command: + command: "{{ item }}" + prompt: + - Exit with uncommitted changes + answer: y + loop: + - configure + - set system syslog file test any any + - exit + +- name: multiple prompt, multiple answer (mandatory check for all prompts) + ansible.netcommon.cli_command: + command: copy sftp sftp://user@host//user/test.img + check_all: true + prompt: + - Confirm download operation + - Password + - Do you want to change that to the standby image + answer: + - y + - + - y + +- name: Simple regexp match for multiple prompt, multiple answer(mandatory check for all prompts) + ansible.netcommon.cli_command: + command: reload in 5 + check_all: true + prompt: + - Save\\? + - confirm + answer: + - n + - y +""" + +RETURN = """ +stdout: + description: The response from the command + returned: when sendonly is false + type: str + sample: 'Version: VyOS 1.1.7[...]' + +json: + description: A dictionary representing a JSON-formatted response + returned: when the device response is valid JSON + type: dict + sample: | + { + "architecture": "i386", + "bootupTimestamp": 1532649700.56, + "modelName": "vEOS", + "version": "4.15.9M" + [...] + } +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import Connection, ConnectionError + + +def main(): + """entry point for module execution""" + argument_spec = dict( + command=dict(type="str", required=True), + prompt=dict(type="list", elements="str", required=False), + answer=dict(type="list", elements="str", required=False), + newline=dict(type="bool", default=True, required=False), + sendonly=dict(type="bool", default=False, required=False), + check_all=dict(type="bool", default=False, required=False), + ) + required_together = [["prompt", "answer"]] + module = AnsibleModule( + argument_spec=argument_spec, + required_together=required_together, + supports_check_mode=True, + ) + + if module.check_mode and not module.params["command"].startswith("show"): + module.fail_json( + msg="Only show commands are supported when using check_mode, not " + "executing %s" % module.params["command"] + ) + + warnings = list() + result = {"changed": False, "warnings": warnings} + + connection = Connection(module._socket_path) + response = "" + try: + response = connection.get(**module.params) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + + if not module.params["sendonly"]: + try: + result["json"] = module.from_json(response) + except ValueError: + pass + + result.update({"stdout": response}) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py new file mode 100644 index 0000000..474971f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_config.py @@ -0,0 +1,456 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, Ansible by 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 = """ +module: cli_config +author: Trishna Guha (@trishnaguha) +notes: +- The commands will be returned only for platforms that do not support onbox diff. + The C(--diff) option with the playbook will return the difference in configuration + for devices that has support for onbox diff +- To ensure idempotency and correct diff the configuration lines in the relevant module + options should be similar to how they appear if present in the running configuration on + device including the indentation. +short_description: Push text based configuration to network devices over network_cli +description: +- This module provides platform agnostic way of pushing text based configuration to + network devices over network_cli connection plugin. +version_added: 1.0.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + config: + description: + - The config to be pushed to the network device. This argument is mutually exclusive + with C(rollback) and either one of the option should be given as input. To ensure + idempotency and correct diff the configuration lines should be similar to how they + appear if present in the running configuration on device including the indentation. + type: str + commit: + description: + - The C(commit) argument instructs the module to push the configuration to the + device. This is mapped to module check mode. + type: bool + replace: + description: + - If the C(replace) argument is set to C(yes), it will replace the entire running-config + of the device with the C(config) argument value. For devices that support replacing + running configuration from file on device like NXOS/JUNOS, the C(replace) argument + takes path to the file on the device that will be used for replacing the entire + running-config. The value of C(config) option should be I(None) for such devices. + Nexus 9K devices only support replace. Use I(net_put) or I(nxos_file_copy) in + case of NXOS module to copy the flat file to remote device and then use set + the fullpath to this argument. + type: str + backup: + description: + - This argument will cause the module to create a full backup of the current running + config from the remote device before any changes are made. If the C(backup_options) + value is not given, the backup file is written to the C(backup) folder in the + playbook root directory or role root directory, if playbook is part of an ansible + role. If the directory does not exist, it is created. + type: bool + default: no + rollback: + description: + - The C(rollback) argument instructs the module to rollback the current configuration + to the identifier specified in the argument. If the specified rollback identifier + does not exist on the remote device, the module will fail. To rollback to the + most recent commit, set the C(rollback) argument to 0. This option is mutually + exclusive with C(config). + type: int + commit_comment: + description: + - The C(commit_comment) argument specifies a text string to be used when committing + the configuration. If the C(commit) argument is set to False, this argument + is silently ignored. This argument is only valid for the platforms that support + commit operation with comment. + type: str + defaults: + description: + - The I(defaults) argument will influence how the running-config is collected + from the device. When the value is set to true, the command used to collect + the running-config is append with the all keyword. When the value is set to + false, the command is issued without the all keyword. + default: no + type: bool + multiline_delimiter: + description: + - This argument is used when pushing a multiline configuration element to the + device. It specifies the character to use as the delimiting character. This + only applies to the configuration action. + type: str + diff_replace: + description: + - Instructs the module on the way to perform the configuration on the device. + If the C(diff_replace) argument is set to I(line) then the modified lines are + pushed to the device in configuration mode. If the argument is set to I(block) + then the entire command block is pushed to the device in configuration mode + if any line is not correct. Note that this parameter will be ignored if the + platform has onbox diff support. + type: str + choices: + - line + - block + - config + diff_match: + description: + - Instructs the module on the way to perform the matching of the set of commands + against the current device config. If C(diff_match) is set to I(line), commands + are matched line by line. If C(diff_match) is set to I(strict), command lines + are matched with respect to position. If C(diff_match) is set to I(exact), command + lines must be an equal match. Finally, if C(diff_match) is set to I(none), the + module will not attempt to compare the source configuration with the running + configuration on the remote device. Note that this parameter will be ignored + if the platform has onbox diff support. + type: str + choices: + - line + - strict + - exact + - none + diff_ignore_lines: + description: + - Use this argument to specify one or more lines that should be ignored during + the diff. This is used for lines in the configuration that are automatically + updated by the system. This argument takes a list of regular expressions or + exact line matches. Note that this parameter will be ignored if the platform + has onbox diff support. + type: list + elements: str + backup_options: + description: + - This is a dict object containing configurable options related to backup file + path. The value of this option is read only when C(backup) is set to I(yes), + if C(backup) is set to I(no) this option will be silently ignored. + suboptions: + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given it will be generated based on the hostname, current time and + date in format defined by _config.@ + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist it will + be first created and the filename is either the value of C(filename) or + default filename as described in C(filename) options description. If the + path value is not given in that case a I(backup) directory will be created + in the current working directory and backup configuration will be copied + in C(filename) within I(backup) directory. + type: path + type: dict +""" + +EXAMPLES = """ +- name: configure device with config + ansible.netcommon.cli_config: + config: "{{ lookup('ansible.builtin.template', 'basic/config.j2') }}" + +- name: multiline config + ansible.netcommon.cli_config: + config: | + hostname foo + feature nxapi + +- name: configure device with config with defaults enabled + ansible.netcommon.cli_config: + config: "{{ lookup('ansible.builtin.template', 'basic/config.j2') }}" + defaults: "yes" + +- name: Use diff_match + ansible.netcommon.cli_config: + config: "{{ lookup('file', 'interface_config') }}" + diff_match: none + +- name: nxos replace config + ansible.netcommon.cli_config: + replace: bootflash:nxoscfg + +- name: junos replace config + ansible.netcommon.cli_config: + replace: /var/home/ansible/junos01.cfg + +- name: commit with comment + ansible.netcommon.cli_config: + config: set system host-name foo + commit_comment: this is a test + +- name: configurable backup path + ansible.netcommon.cli_config: + config: "{{ lookup('ansible.builtin.template', 'basic/config.j2') }}" + backup: "yes" + backup_options: + filename: backup.cfg + dir_path: /home/user +""" + +RETURN = """ +diff: + description: The diff generated on the device when the commands were applied + returned: When I(supports_onbox_diff=True) in the platform's cliconf plugin + type: str + sample: |- + --- system:/running-config + +++ session:/ansible_1599745461-session-config + @@ -4,7 +4,7 @@ + ! + transceiver qsfp default-mode 4x10G + ! + -hostname veos + +hostname veos3 + ! + spanning-tree mode mstp +commands: + description: The set of commands that will be pushed to the remote device + returned: When I(supports_generated_diff=True) and I(supports_onbox_diff=False) in the platform's cliconf plugin + type: list + sample: ['interface Loopback999', 'no shutdown'] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: str + sample: /playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34 +""" + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import Connection + + +def validate_args(module, device_operations): + """validate param if it is supported on the platform""" + feature_list = [ + "replace", + "rollback", + "commit_comment", + "defaults", + "multiline_delimiter", + "diff_replace", + "diff_match", + "diff_ignore_lines", + ] + + for feature in feature_list: + if module.params[feature]: + supports_feature = device_operations.get("supports_%s" % feature) + if supports_feature is None: + module.fail_json( + msg="This platform does not specify whether %s is supported or not. " + "Please report an issue against this platform's cliconf plugin." % feature + ) + elif not supports_feature: + module.fail_json(msg="Option %s is not supported on this platform" % feature) + + +def run(module, device_operations, connection, candidate, running, rollback_id): + result = {} + resp = {} + config_diff = [] + banner_diff = {} + + replace = module.params["replace"] + commit_comment = module.params["commit_comment"] + multiline_delimiter = module.params["multiline_delimiter"] + diff_replace = module.params["diff_replace"] + diff_match = module.params["diff_match"] + diff_ignore_lines = module.params["diff_ignore_lines"] + + commit = not module.check_mode + + if replace in ("yes", "true", "True"): + replace = True + elif replace in ("no", "false", "False"): + replace = False + + if replace is not None and replace not in [True, False] and candidate is not None: + module.fail_json( + msg="Replace value '%s' is a configuration file path already" + " present on the device. Hence 'replace' and 'config' options" + " are mutually exclusive" % replace + ) + + if rollback_id is not None: + resp = connection.rollback(rollback_id, commit) + if "diff" in resp: + result["changed"] = True + + elif device_operations.get("supports_onbox_diff"): + if diff_replace: + module.warn("diff_replace is ignored as the device supports onbox diff") + if diff_match: + module.warn("diff_match is ignored as the device supports onbox diff") + if diff_ignore_lines: + module.warn("diff_ignore_lines is ignored as the device supports onbox diff") + + if candidate and not isinstance(candidate, list): + candidate = candidate.strip("\n").splitlines() + + kwargs = { + "candidate": candidate, + "commit": commit, + "replace": replace, + "comment": commit_comment, + } + resp = connection.edit_config(**kwargs) + + if "diff" in resp: + result["changed"] = True + result["diff"] = resp["diff"] + + elif device_operations.get("supports_generate_diff"): + kwargs = {"candidate": candidate, "running": running} + if diff_match: + kwargs.update({"diff_match": diff_match}) + if diff_replace: + kwargs.update({"diff_replace": diff_replace}) + if diff_ignore_lines: + kwargs.update({"diff_ignore_lines": diff_ignore_lines}) + + diff_response = connection.get_diff(**kwargs) + + config_diff = diff_response.get("config_diff") + banner_diff = diff_response.get("banner_diff") + + if config_diff: + if isinstance(config_diff, list): + candidate = config_diff + else: + candidate = config_diff.splitlines() + + kwargs = { + "candidate": candidate, + "commit": commit, + "replace": replace, + "comment": commit_comment, + } + if commit: + connection.edit_config(**kwargs) + result["changed"] = True + result["commands"] = config_diff.split("\n") + + if banner_diff: + candidate = json.dumps(banner_diff) + + kwargs = {"candidate": candidate, "commit": commit} + if multiline_delimiter: + kwargs.update({"multiline_delimiter": multiline_delimiter}) + if commit: + connection.edit_banner(**kwargs) + result["changed"] = True + + if module._diff: + if "diff" in resp: + result["diff"] = {"prepared": resp["diff"]} + else: + diff = "" + if config_diff: + if isinstance(config_diff, list): + diff += "\n".join(config_diff) + else: + diff += config_diff + if banner_diff: + diff += json.dumps(banner_diff) + result["diff"] = {"prepared": diff} + + if result.get("changed"): + msg = ( + "To ensure idempotency and correct diff the input configuration lines should be" + " similar to how they appear if present in" + " the running configuration on device including the indentation" + ) + if "warnings" in result: + result["warnings"].append(msg) + else: + result["warnings"] = msg + return result + + +def main(): + """main entry point for execution""" + backup_spec = dict(filename=dict(), dir_path=dict(type="path")) + argument_spec = dict( + backup=dict(default=False, type="bool"), + backup_options=dict(type="dict", options=backup_spec), + config=dict(type="str"), + commit=dict(type="bool"), + replace=dict(type="str"), + rollback=dict(type="int"), + commit_comment=dict(type="str"), + defaults=dict(default=False, type="bool"), + multiline_delimiter=dict(type="str"), + diff_replace=dict(choices=["line", "block", "config"]), + diff_match=dict(choices=["line", "strict", "exact", "none"]), + diff_ignore_lines=dict(type="list", elements="str"), + ) + + mutually_exclusive = [("config", "rollback")] + required_one_of = [["backup", "config", "rollback"]] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + required_one_of=required_one_of, + supports_check_mode=True, + ) + + result = {"changed": False} + + connection = Connection(module._socket_path) + capabilities = module.from_json(connection.get_capabilities()) + + if capabilities: + device_operations = capabilities.get("device_operations", dict()) + validate_args(module, device_operations) + else: + device_operations = dict() + + if module.params["defaults"]: + if "get_default_flag" in capabilities.get("rpc"): + flags = connection.get_default_flag() + else: + flags = "all" + else: + flags = [] + + candidate = module.params["config"] + candidate = to_text(candidate, errors="surrogate_then_replace") if candidate else None + running = connection.get_config(flags=flags) + rollback_id = module.params["rollback"] + + if module.params["backup"]: + result["__backup__"] = running + + if candidate or rollback_id is not None or module.params["replace"]: + try: + result.update( + run( + module, + device_operations, + connection, + candidate, + running, + rollback_id, + ) + ) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_restore.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_restore.py new file mode 100644 index 0000000..538868c --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/cli_restore.py @@ -0,0 +1,128 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2024, Ansible by 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 = """ +module: cli_restore +author: Sagar Paul (@KB-perByte) +short_description: Restore device configuration to network devices over network_cli +description: +- This module provides platform agnostic way of restore text based configuration to + network devices over network_cli connection plugin. +- The module uses the platforms `config replace` commands to restore + backup configuration that is already copied over to the appliance. +version_added: 6.1.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + filename: + description: + - Filename of the backup file, present in the appliance where the restore operation + is to be performed. Check appliance for the configuration backup file name. + type: str + path: + description: + - The location in the target appliance where the file containing the backup exists. + The path and the filename together create the input to the config replace command, + - For an IOSXE appliance the path pattern is flash:// + type: str +""" + +EXAMPLES = """ +- name: Restore IOS-XE configuration + ansible.netcommon.cli_restore: + filename: backupDday.cfg + path: flash:// + +# Command fired +# ------------- +# config replace flash://backupDday.cfg force + +# Task Output +# ----------- +# +# ok: [BATMON] => changed=false +# __restore__: |- +# The rollback configlet from the last pass is listed below: +# ******** +# !List of Rollback Commands: +# Building configuration... +# Current configuration : 3781 bytes +# end +# ******** +# +# +# Rollback aborted after 5 passes +# The following commands are failed to apply to the IOS image. +# ******** +# Building configuration... +# Current configuration : 3781 bytes +# ******** +# invocation: +# module_args: +# filename: backupDday.cfg +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import Connection, ConnectionError + + +def validate_args(module, device_operations): + """validate param if it is supported on the platform""" + feature_list = [] + + for feature in feature_list: + if module.params[feature]: + supports_feature = device_operations.get(f"supports_{feature}") + if supports_feature is None: + module.fail_json( + msg=f"This platform does not specify whether {feature} is supported or not. " + "Please report an issue against this platform's cliconf plugin." + ) + elif not supports_feature: + module.fail_json(msg=f"Option {feature} is not supported on this platform") + + +def main(): + """main entry point for execution""" + argument_spec = dict( + filename=dict(type="str"), + path=dict(type="str"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + ) + + result = {"changed": False} + connection = Connection(module._socket_path) + try: + running = connection.restore( + filename=module.params["filename"], + path=module.params["path"], + ) + except ConnectionError as exc: + if exc.code == -32601: # Method not found + msg = "This platform is not supported with cli_restore. Please report an issue against this platform's cliconf plugin." + module.fail_json(msg, code=exc.code) + else: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace").strip()) + result["__restore__"] = running + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/grpc_config.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/grpc_config.py new file mode 100644 index 0000000..31f67f8 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/grpc_config.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2022 Red Hat +# GNU General Public License v3.0+ +# 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: grpc_config +version_added: "3.1.0" +author: + - "Gomathi Selvi S (@GomathiselviS)" +short_description: Fetch configuration/state data from gRPC enabled target hosts. +description: + - gRPC is a high performance, open-source universal RPC framework. + - This module allows the user to append configs to an existing configuration in a gRPC + enabled devices. +options: + config: + description: + - This option specifies the string which acts as a filter to restrict the portions of + the data to be retrieved from the target host device. If this option is not specified the entire + configuration or state data is returned in response provided it is supported by target host. + type: str + state: + description: action to be performed + type: str + backup: + description: + - This argument will cause the module to create a full backup of the current C(running-config) + from the remote device before any changes are made. If the C(backup_options) + value is not given, the backup file is written to the C(backup) folder in the + playbook root directory or role root directory, if playbook is part of an ansible + role. If the directory does not exist, it is created. + type: bool + default: no + backup_options: + description: + - This is a dict object containing configurable options related to backup file + path. The value of this option is read only when C(backup) is set to I(yes), + if C(backup) is set to I(no) this option will be silently ignored. + suboptions: + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given it will be generated based on the hostname, current time and + date in format defined by _config.@ + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist it will + be first created and the filename is either the value of C(filename) or + default filename as described in C(filename) options description. If the + path value is not given in that case a I(backup) directory will be created + in the current working directory and backup configuration will be copied + in C(filename) within I(backup) directory. + type: path + type: dict + +requirements: + - grpcio + - protobuf +notes: + - This module requires the gRPC system service be enabled on + the target host being managed. + - This module supports the use of connection=connection=ansible.netcommon.grpc + - This module requires the value of 'ansible_network_os' or 'grpc_type' configuration option + (refer ansible.netcommon.grpc connection plugin documentation) + be defined as an inventory variable. + - Tested against iosxrv 9k version 6.1.2. +""" + +EXAMPLES = """ +- name: Merge static route config + ansible.netcommon.grpc_config: + config: + Cisco-IOS-XR-ip-static-cfg:router-static: + default-vrf: + address-family: + vrfipv4: + vrf-unicast: + vrf-prefixes: + vrf-prefix: + - prefix: "1.2.3.6" + prefix-length: 32 + vrf-route: + vrf-next-hop-table: + vrf-next-hop-next-hop-address: + - next-hop-address: "10.0.2.2" + state: merged + +- name: Merge bgp config + ansible.netcommon.grpc_config: + config: "{{ lookup('file', 'bgp.json') }}" + state: merged + +- name: Find diff + diff: true + ansible.netcommon.grpc_config: + config: "{{ lookup('file', 'bgp_start.yml') }}" + state: merged + +- name: Backup running config + ansible.netcommon.grpc_config: + backup: true +""" + +RETURN = """ +stdout: + description: The raw string containing response object + received from the gRPC server. + returned: error mesage, when failure happens. + empty , when the operation is successful + type: str + 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: ['...', '...'] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: str + sample: /playbooks/ansible/backup/config.2022-07-16@22:28:34 +diff: + description: If --diff option in enabled while running, the before and after configuration change are + returned as part of before and after key. + returned: when diff is enabled + type: dict +""" +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import ConnectionError + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_diff, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.grpc.grpc import ( + delete_config, + merge_config, + replace_config, + run_cli, + sanitize_content, + validate_config, +) + + +try: + import yaml + + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +def main(): + """entry point for module execution""" + backup_spec = dict(filename=dict(), dir_path=dict(type="path")) + argument_spec = dict( + config=dict(type="str"), + state=dict(type="str"), + backup=dict(type="bool", default=False), + backup_options=dict(type="dict", options=backup_spec), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + config = {} + if module.params["config"]: + config = json.dumps(yaml.safe_load(module.params["config"])) + config = json.loads(config) + state = module.params["state"] + + result = { + "changed": False, + } + before = None + after = None + before_diff = None + after_diff = None + output = "" + try: + if module.params["backup"] or state in [ + "merged", + "replaced", + "deleted", + ]: + if not module.check_mode: + response, err = run_cli(module, "show running-config", "text") + before = to_text(response, errors="surrogate_then_replace").strip() + + if module._diff or module.check_mode: + before_diff = validate_config(module, config) + if module.check_mode: + diff = dict_diff(before_diff, config) + if diff: + result["changed"] = True + result["diff"] = diff + else: + if module.params["backup"]: + result["__backup__"] = before.strip() + if state == "merged": + output = merge_config(module, config) + elif state == "replaced": + output = replace_config(module, config) + elif state == "deleted": + output = delete_config(module, config) + if state: + response, err = run_cli(module, "show running-config", "text") + after = to_text(response, errors="surrogate_then_replace").strip() + if before: + before = sanitize_content(before) + if after: + after = sanitize_content(after) + if before != after: + result["changed"] = True + if module._diff: + after_diff = validate_config(module, config) + result["diff"] = { + "before": before_diff, + "after": after_diff, + } + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace"), code=exc.code) + + result["stdout"] = output + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/grpc_get.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/grpc_get.py new file mode 100644 index 0000000..9a55a82 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/grpc_get.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2022 Red Hat +# GNU General Public License v3.0+ +# 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: grpc_get +version_added: "3.1.0" +author: + - "Ganesh Nalawade (@ganeshrn)" + - "Gomathi Selvi S (@GomathiselviS)" +short_description: Fetch configuration/state data from gRPC enabled target hosts. +description: + - gRPC is a high performance, open-source universal RPC framework. + - This module allows the user to fetch configuration and state data from gRPC + enabled devices. +options: + section: + description: + - This option specifies the string which acts as a filter to restrict the portions of + the data to be retrieved from the target host device. If this option is not specified the entire + configuration or state data is returned in response provided it is supported by target host. + aliases: + - filter + type: str + command: + description: + - The option specifies the command to be executed on the target host and return the response + in result. This option is supported if the gRPC target host supports executing CLI command + over the gRPC connection. + type: str + display: + description: + - Encoding scheme to use when serializing output from the device. The encoding scheme + value depends on the capability of the gRPC server running on the target host. + The values can be I(json), I(text) etc. + type: str + data_type: + description: + - The type of data that should be fetched from the target host. The value depends on the + capability of the gRPC server running on target host. The values can be I(config), I(oper) + etc. based on what is supported by the gRPC server. By default it will return both configuration + and operational state data in response. + type: str +requirements: + - grpcio + - protobuf +notes: + - This module requires the gRPC system service be enabled on + the target host being managed. + - This module supports the use of connection=ansible.netcommon.grpc. + - This module requires the value of 'ansible_network_os or grpc_type' configuration option (refer ansible.netcommon.grpc + connection plugin documentation) be defined as an inventory variable. + - Tested against iosxrv 9k version 6.1.2. +""" + +EXAMPLES = """ +- name: Get bgp configuration data + grpc_get: + section: + Cisco-IOS-XR-ip-static-cfg:router-static: + - null +- name: run cli command + grpc_get: + command: "show version" + display: text +""" + +RETURN = """ +stdout: + description: The raw string containing configuration or state data + received from the gRPC server. + returned: always apart from low-level errors (such as action plugin) + type: str + 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: ['...', '...'] +output: + description: A dictionary representing a JSON-formatted response, if the response + is a valid json string + returned: when the device response is valid JSON + type: list + sample: | + [{ + "Cisco-IOS-XR-ip-static-cfg:router-static": { + "default-vrf": { + "address-family": { + "vrfipv4": { + "vrf-unicast": { + "vrf-prefixes": { + "vrf-prefix": [ + { + "prefix": "0.0.0.0", + "prefix-length": 0, + "vrf-route": { + "vrf-next-hop-table": { + "vrf-next-hop-interface-name-next-hop-address": [ + { + "interface-name": "MgmtEth0/RP0/CPU0/0", + "next-hop-address": "10.0.2.2" + } + ] + } + } + } + ] + } + } + } + } + } + } + }] +""" +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import ConnectionError + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list +from ansible_collections.ansible.netcommon.plugins.module_utils.network.grpc.grpc import ( + get, + get_capabilities, + run_cli, +) + + +try: + import yaml + + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +def main(): + """entry point for module execution""" + argument_spec = dict( + section=dict(type="str", aliases=["filter"]), + command=dict(type="str"), + display=dict(type="str"), + data_type=dict(type="str"), + ) + + mutually_exclusive = [["section", "command"]] + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + capabilities = get_capabilities(module) + + operations = capabilities["server_capabilities"] + + if module.params["section"]: + section = json.dumps(yaml.safe_load(module.params["section"])) + else: + section = None + command = module.params["command"] + display = module.params["display"] + data_type = module.params["data_type"] + + supported_display = operations.get("display", []) + if display and display not in supported_display: + module.fail_json( + msg="display option '%s' is not supported on this target host. Valid value is one of '%s'" + % (display, supported_display.join(", ")) + ) + + if command and not operations.get("supports_cli_command", False): + module.fail_json(msg="command option '%s' is not supported on this target host" % command) + + supported_data_type = operations.get("data_type", []) + if data_type and data_type not in supported_data_type: + module.fail_json( + msg="data_type option '%s' is not supported on this target host. Valid value is one of %s" + % (data_type, supported_data_type.join(",")) + ) + + result = {"changed": False} + output = [] + try: + if command: + response, err = run_cli(module, command, display) + else: + response, err = get(module, section, data_type) + try: + output = json.loads(response) + except ValueError: + module.warn(to_text(err, errors="surrogate_then_replace")) + + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace"), code=exc.code) + result["stdout"] = response + + if output: + result["output"] = to_list(output) + else: + result["output"] = to_list(response) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/net_get.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/net_get.py new file mode 100644 index 0000000..f7fee68 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/net_get.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, Ansible by 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 = """ +module: net_get +author: Deepak Agrawal (@dagrawal) +short_description: Copy a file from a network device to Ansible Controller +description: +- This module provides functionality to copy file from network device to ansible controller. +version_added: 1.0.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + src: + description: + - Specifies the source file. The path to the source file can either be the full + path on the network device or a relative path as per path supported by destination + network device. + required: true + protocol: + description: + - Protocol used to transfer file. + default: scp + choices: + - scp + - sftp + dest: + description: + - Specifies the destination file. The path to the destination file can either + be the full path on the Ansible control host or a relative path from the playbook + or role root directory. + default: + - Same filename as specified in I(src). The path will be playbook root or role + root directory if playbook is part of a role. +requirements: +- scp if using protocol=scp with paramiko +notes: +- Some devices need specific configurations to be enabled before scp can work These + configuration should be pre-configured before using this module e.g ios - C(ip scp + server enable). +- User privilege to do scp on network device should be pre-configured e.g. ios - need + user privilege 15 by default for allowing scp. +- Default destination of source file. +""" + +EXAMPLES = """ +- name: copy file from the network device to Ansible controller + ansible.netcommon.net_get: + src: running_cfg_ios1.txt + +- name: copy file from ios to common location at /tmp + ansible.netcommon.net_get: + src: running_cfg_sw1.txt + dest: /tmp/ios1.txt +""" + +RETURN = """ +""" diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/net_ping.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/net_ping.py new file mode 100644 index 0000000..1992be4 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/net_ping.py @@ -0,0 +1,106 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by 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 = """ +module: net_ping +author: Jacob McGill (@jmcgill298) +short_description: Tests reachability using ping from a network device +description: +- Tests reachability using ping from network device to a remote destination. +version_added: 1.0.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + count: + description: + - Number of packets to send. + default: 5 + dest: + description: + - The IP Address or hostname (resolvable by switch) of the remote node. + required: true + source: + description: + - The source IP Address. + state: + description: + - Determines if the expected result is success or fail. + choices: + - absent + - present + default: present + vrf: + description: + - The VRF to use for forwarding. + default: default +notes: +- For targets running Python, use the M(ansible.builtin.shell) module along with ping command instead. +""" + + +EXAMPLES = """ +- name: Test reachability to 10.10.10.10 using default vrf + ansible.netcommon.net_ping: + dest: 10.10.10.10 + +- name: Test reachability to 10.20.20.20 using prod vrf + ansible.netcommon.net_ping: + dest: 10.20.20.20 + vrf: prod + +- name: Test unreachability to 10.30.30.30 using default vrf + ansible.netcommon.net_ping: + dest: 10.30.30.30 + state: absent + +- name: Test reachability to 10.40.40.40 using prod vrf and setting count and source + ansible.netcommon.net_ping: + dest: 10.40.40.40 + source: loopback0 + vrf: prod + count: 20 + +- Note: + - For targets running Python, use the M(ansible.builtin.shell) module along with ping command instead. + - Example: + name: ping + shell: ping -c 1 +""" + +RETURN = r""" +commands: + description: Show the command sent. + returned: always + type: list + sample: ["ping vrf prod 10.40.40.40 count 20 source loopback0"] +packet_loss: + description: Percentage of packets lost. + returned: always + type: str + sample: "0%" +packets_rx: + description: Packets successfully received. + returned: always + type: int + sample: 20 +packets_tx: + description: Packets successfully transmitted. + returned: always + type: int + sample: 20 +rtt: + description: Show RTT stats. + returned: always + type: dict + sample: {"avg": 2, "max": 8, "min": 1} +""" diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/net_put.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/net_put.py new file mode 100644 index 0000000..a095eb6 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/net_put.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, Ansible by 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 = """ +module: net_put +author: Deepak Agrawal (@dagrawal) +short_description: Copy a file from Ansible Controller to a network device +description: +- This module provides functionality to copy file from Ansible controller to network + devices. +version_added: 1.0.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + src: + description: + - Specifies the source file. The path to the source file can either be the full + path on the Ansible control host or a relative path from the playbook or role + root directory. + required: true + protocol: + description: + - Protocol used to transfer file. + default: scp + choices: + - scp + - sftp + dest: + description: + - Specifies the destination file. The path to destination file can either be the + full path or relative path as supported by network_os. + default: + - Filename from src and at default directory of user shell on network_os. + required: false + mode: + description: + - Set the file transfer mode. If mode is set to I(text) then I(src) file will + go through Jinja2 template engine to replace any vars if present in the src + file. If mode is set to I(binary) then file will be copied as it is to destination + device. + default: binary + choices: + - binary + - text +requirements: +- scp if using protocol=scp with paramiko +notes: +- Some devices need specific configurations to be enabled before scp can work These + configuration should be pre-configured before using this module e.g ios - C(ip scp + server enable). +- User privilege to do scp on network device should be pre-configured e.g. ios - need + user privilege 15 by default for allowing scp. +- Default destination of source file. +""" + +EXAMPLES = """ +- name: copy file from ansible controller to a network device + ansible.netcommon.net_put: + src: running_cfg_ios1.txt + +- name: copy file at root dir of flash in slot 3 of sw1(ios) + ansible.netcommon.net_put: + src: running_cfg_sw1.txt + protocol: sftp + dest: flash3:/running_cfg_sw1.txt +""" + +RETURN = """ +""" diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_config.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_config.py new file mode 100644 index 0000000..1d6f25e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_config.py @@ -0,0 +1,703 @@ +#!/usr/bin/python + +# (c) 2016, Leandro Lisboa Penz +# 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: netconf_config +author: + - Leandro Lisboa Penz (@lpenz) + - Ganesh Nalawade (@ganeshrn) +short_description: netconf device configuration +description: + - Netconf is a network management protocol developed and standardized by the IETF. + It is documented in RFC 6241. + - This module allows the user to send a configuration XML file to a netconf device, + and detects if there was a configuration change. +version_added: 1.0.0 +extends_documentation_fragment: + - ansible.netcommon.network_agnostic +options: + content: + description: + - The configuration data as defined by the device's data models, the value can + be either in xml string format or text format or python dictionary representation of JSON format. + - In case of json string format it will be converted to the corresponding xml string using + xmltodict library before pushing onto the remote host. + - In case the value of this option isn I(text) format the format should be supported by remote Netconf server. + - If the value of C(content) option is in I(xml) format in that case the xml value should + have I(config) as root tag. + type: raw + aliases: + - xml + target: + description: + Name of the configuration datastore to be edited. - auto, uses candidate + and fallback to running - candidate, edit datastore and then commit + - running, edit datastore directly + default: auto + type: str + choices: + - auto + - candidate + - running + aliases: + - datastore + source_datastore: + description: + - Name of the configuration datastore to use as the source to copy the configuration + to the datastore mentioned by C(target) option. The values can be either I(running), + I(candidate), I(startup) or a remote URL + type: str + aliases: + - source + format: + description: + - The format of the configuration provided as value of C(content). + - In case of json string format it will be converted to the corresponding xml string using + xmltodict library before pushing onto the remote host. + - In case of I(text) format of the configuration should be supported by remote Netconf server. + - If the value of C(format) options is not given it tries to guess the data format of + C(content) option as one of I(xml) or I(json) or I(text). + - If the data format is not identified it is set to I(xml) by default. + type: str + choices: + - xml + - text + - json + lock: + description: + - Instructs the module to explicitly lock the datastore specified as C(target). + By setting the option value I(always) is will explicitly lock the datastore + mentioned in C(target) option. It the value is I(never) it will not lock the + C(target) datastore. The value I(if-supported) lock the C(target) datastore + only if it is supported by the remote Netconf server. + type: str + default: always + choices: + - never + - always + - if-supported + default_operation: + description: + - The default operation for rpc, valid values are I(merge), I(replace) + and I(none). If the default value is merge, the configuration data in the C(content) + option is merged at the corresponding level in the C(target) datastore. If the + value is replace the data in the C(content) option completely replaces the configuration + in the C(target) datastore. If the value is none the C(target) datastore is + unaffected by the configuration in the config option, unless and until the incoming + configuration data uses the C(operation) operation to request a different operation. + type: str + choices: + - merge + - replace + - none + confirm: + description: + - This argument will configure a timeout value for the commit to be confirmed + before it is automatically rolled back. If the C(confirm_commit) argument is + set to False, this argument is silently ignored. If the value of this argument + is set to 0, the commit is confirmed immediately. The remote host MUST support + :candidate and :confirmed-commit capability for this option to . + type: int + default: 0 + confirm_commit: + description: + - This argument will execute commit operation on remote device. It can be used + to confirm a previous commit. + type: bool + default: no + error_option: + description: + - This option controls the netconf server action after an error occurs while editing + the configuration. + - If I(error_option=stop-on-error), abort the config edit on first error. + - If I(error_option=continue-on-error), continue to process configuration data + on error. The error is recorded and negative response is generated if any errors + occur. + - If I(error_option=rollback-on-error), rollback to the original configuration + if any error occurs. This requires the remote Netconf server to support the + I(error_option=rollback-on-error) capability. + default: stop-on-error + type: str + choices: + - stop-on-error + - continue-on-error + - rollback-on-error + save: + description: + - The C(save) argument instructs the module to save the configuration in C(target) + datastore to the startup-config if changed and if :startup capability is supported + by Netconf server. + default: false + type: bool + backup: + description: + - This argument will cause the module to create a full backup of the current C(running-config) + from the remote device before any changes are made. If the C(backup_options) + value is not given, the backup file is written to the C(backup) folder in the + playbook root directory or role root directory, if playbook is part of an ansible + role. If the directory does not exist, it is created. + type: bool + default: no + delete: + description: + - It instructs the module to delete the configuration from value mentioned in + C(target) datastore. + type: bool + default: no + commit: + description: + - This boolean flag controls if the configuration changes should be committed + or not after editing the candidate datastore. This option is supported only + if remote Netconf server supports :candidate capability. If the value is set + to I(False) commit won't be issued after edit-config operation and user needs + to handle commit or discard-changes explicitly. + type: bool + default: true + validate: + description: + - This boolean flag if set validates the content of datastore given in C(target) + option. For this option to work remote Netconf server should support :validate + capability. + type: bool + default: false + backup_options: + description: + - This is a dict object containing configurable options related to backup file + path. The value of this option is read only when C(backup) is set to I(yes), + if C(backup) is set to I(no) this option will be silently ignored. + suboptions: + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given it will be generated based on the hostname, current time and + date in format defined by _config.@ + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist it will + be first created and the filename is either the value of C(filename) or + default filename as described in C(filename) options description. If the + path value is not given in that case a I(backup) directory will be created + in the current working directory and backup configuration will be copied + in C(filename) within I(backup) directory. + type: path + type: dict + get_filter: + description: + - This argument specifies the XML string which acts as a filter to restrict the + portions of the data retrieved from the remote device when comparing the before + and after state of the device following calls to edit_config. When not specified, + the entire configuration or state data is returned for comparison depending + on the value of C(source) option. The C(get_filter) value can be either XML + string or XPath or JSON string or native python dictionary, if the filter is + in XPath format the NETCONF server running on remote host should support xpath + capability else it will result in an error. + type: raw +requirements: + - ncclient +notes: + - This module requires the netconf system service be enabled on the remote device + being managed. + - This module supports devices with and without the candidate and confirmed-commit + capabilities. It will always use the safer feature. + - This module supports the use of connection=netconf +""" + +EXAMPLES = """ +- name: use lookup filter to provide xml configuration + ansible.netcommon.netconf_config: + content: "{{ lookup('file', './config.xml') }}" + +- name: set ntp server in the device + ansible.netcommon.netconf_config: + content: | + + + + true + + ntp1 +
127.0.0.1
+
+
+
+
+ +- name: wipe ntp configuration + ansible.netcommon.netconf_config: + content: | + + + + false + + ntp1 + + + + + +- name: configure interface while providing different private key file path (for connection=netconf) + ansible.netcommon.netconf_config: + backup: true + register: backup_junos_location + vars: + ansible_private_key_file: /home/admin/.ssh/newprivatekeyfile + +- name: configurable backup path + ansible.netcommon.netconf_config: + backup: true + backup_options: + filename: backup.cfg + dir_path: /home/user + +- name: "configure using direct native format configuration (cisco iosxr)" + ansible.netcommon.netconf_config: + format: json + content: + { + "config": + { + "interface-configurations": + { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": + { + "active": "act", + "description": "test for ansible Loopback999", + "interface-name": "Loopback999", + }, + }, + }, + } + get_filter: + { + "interface-configurations": + { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": null, + }, + } + +- name: "configure using json string format configuration (cisco iosxr)" + ansible.netcommon.netconf_config: + format: json + content: | + { + "config": { + "interface-configurations": { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": { + "active": "act", + "description": "test for ansible Loopback999", + "interface-name": "Loopback999" + } + } + } + } + get_filter: | + { + "interface-configurations": { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": null + } + } + +# Make a round-trip interface description change, diff the before and after +# this demonstrates the use of the native display format and several utilities +# from the ansible.utils collection + +- name: Define the openconfig interface filter + set_fact: + filter: + interfaces: + "@xmlns": "http://openconfig.net/yang/interfaces" + interface: + name: Ethernet2 + +- name: Get the pre-change config using the filter + ansible.netcommon.netconf_get: + source: running + filter: "{{ filter }}" + display: native + register: pre + +- name: Update the description + ansible.utils.update_fact: + updates: + - path: pre.output.data.interfaces.interface.config.description + value: "Configured by ansible {{ 100 | random }}" + register: updated + +- name: Apply the new configuration + ansible.netcommon.netconf_config: + content: + config: + interfaces: "{{ updated.pre.output.data.interfaces }}" + +- name: Get the post-change config using the filter + ansible.netcommon.netconf_get: + source: running + filter: "{{ filter }}" + display: native + register: post + +- name: Show the differences between the pre and post configurations + ansible.utils.fact_diff: + before: "{{ pre.output.data|ansible.utils.to_paths }}" + after: "{{ post.output.data|ansible.utils.to_paths }}" +# TASK [Show the differences between the pre and post configurations] ******** +# --- before +# +++ after +# @@ -1,11 +1,11 @@ +# { +# - "@time-modified": "2020-10-23T12:27:17.462332477Z", +# + "@time-modified": "2020-10-23T12:27:21.744541708Z", +# "@xmlns": "urn:ietf:params:xml:ns:netconf:base:1.0", +# "interfaces.interface.aggregation.config['fallback-timeout']['#text']": "90", +# "interfaces.interface.aggregation.config['fallback-timeout']['@xmlns']": "http://arista.com/yang/openconfig/interfaces/augments", +# "interfaces.interface.aggregation.config['min-links']": "0", +# "interfaces.interface.aggregation['@xmlns']": "http://openconfig.net/yang/interfaces/aggregate", +# - "interfaces.interface.config.description": "Configured by ansible 56", +# + "interfaces.interface.config.description": "Configured by ansible 67", +# "interfaces.interface.config.enabled": "true", +# "interfaces.interface.config.mtu": "0", +# "interfaces.interface.config.name": "Ethernet2", +""" + +RETURN = """ +server_capabilities: + description: list of capabilities of the server + returned: success + type: list + sample: + [ + "urn:ietf:params:netconf:base:1.1", + "urn:ietf:params:netconf:capability:confirmed-commit:1.0", + "urn:ietf:params:netconf:capability:candidate:1.0", + ] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: str + sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34 +diff: + description: + If --diff option in enabled while running, the before and after configuration change are + returned as part of before and after key. + returned: when diff is enabled + type: dict + sample: + "after": "\n\n\n17.3R1.10...<--snip-->" + "before": "\n\n\n 17.3R1.10...<--snip-->" + +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import Connection, ConnectionError + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.netconf.netconf import ( + get_capabilities, + get_config, + sanitize_xml, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.utils.data import ( + dict_to_xml, + validate_and_normalize_data, +) + + +try: + from lxml.etree import fromstring, tostring +except ImportError: + from xml.etree.ElementTree import fromstring, tostring + + +def validate_config(module, config, format="xml"): + if format == "xml": + try: + root = fromstring(config) + if not root.tag.endswith("config"): + module.fail_json( + msg="content value should have XML string with config tag as the root node" + ) + except Exception as exc: + module.fail_json( + "Value of content option is invalid as per the identified format %s, validation failed with error: %s" + % (format, to_text(exc, errors="surrogate_then_replace")) + ) + + +def main(): + """main entry point for module execution""" + backup_spec = dict(filename=dict(), dir_path=dict(type="path")) + argument_spec = dict( + content=dict(aliases=["xml"], type="raw"), + target=dict( + choices=["auto", "candidate", "running"], + default="auto", + aliases=["datastore"], + ), + source_datastore=dict(aliases=["source"]), + format=dict(choices=["xml", "text", "json"]), + lock=dict(choices=["never", "always", "if-supported"], default="always"), + default_operation=dict(choices=["merge", "replace", "none"]), + confirm=dict(type="int", default=0), + confirm_commit=dict(type="bool", default=False), + error_option=dict( + choices=[ + "stop-on-error", + "continue-on-error", + "rollback-on-error", + ], + default="stop-on-error", + ), + backup=dict(type="bool", default=False), + backup_options=dict(type="dict", options=backup_spec), + save=dict(type="bool", default=False), + delete=dict(type="bool", default=False), + commit=dict(type="bool", default=True), + validate=dict(type="bool", default=False), + get_filter=dict(type="raw"), + ) + + mutually_exclusive = [("content", "source_datastore", "delete", "confirm_commit")] + required_one_of = [("content", "source_datastore", "delete", "confirm_commit")] + + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + config = module.params["content"] + target = module.params["target"] + lock = module.params["lock"] + source = module.params["source_datastore"] + delete = module.params["delete"] + confirm_commit = module.params["confirm_commit"] + confirm = module.params["confirm"] + validate = module.params["validate"] + save = module.params["save"] + filter = module.params["get_filter"] + format = module.params["format"] + + try: + filter_data, filter_type = validate_and_normalize_data(filter) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + if filter_type == "xml": + filter_type = "subtree" + elif filter_type == "json": + try: + filter = dict_to_xml(filter_data) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + filter_type = "subtree" + elif filter_type == "xpath": + pass + elif filter_type is None: + if filter_data is not None: + # to maintain backward compatibility for ansible 2.9 which + # defaults to "subtree" filter type + filter_type = "subtree" + module.warn( + "The data format of get_filter option value couldn't be identified, hence set to 'subtree'" + ) + else: + pass + else: + module.fail_json( + msg="Invalid filter type detected %s for get_filter value %s" % (filter_type, filter) + ) + + conn = Connection(module._socket_path) + capabilities = get_capabilities(module) + operations = capabilities["device_operations"] + + supports_commit = operations.get("supports_commit", False) + supports_writable_running = operations.get("supports_writable_running", False) + supports_startup = operations.get("supports_startup", False) + + # identify target datastore + if target == "candidate" and not supports_commit: + module.fail_json(msg=":candidate is not supported by this netconf server") + elif target == "running" and not supports_writable_running: + module.fail_json(msg=":writable-running is not supported by this netconf server") + elif target == "auto": + if supports_commit: + target = "candidate" + elif supports_writable_running: + target = "running" + else: + module.fail_json( + msg="neither :candidate nor :writable-running are supported by this netconf server" + ) + + # Netconf server capability validation against input options + if save and not supports_startup: + module.fail_json( + msg="cannot copy <%s/> to , while :startup is not supported" % target + ) + + if confirm_commit and not operations.get("supports_confirm_commit", False): + module.fail_json(msg="confirm commit is not supported by Netconf server") + + if (confirm > 0) and not operations.get("supports_confirm_commit", False): + module.fail_json( + msg="confirm commit is not supported by this netconf server, given confirm timeout: %d" + % confirm + ) + + if validate and not operations.get("supports_validate", False): + module.fail_json(msg="validate is not supported by this netconf server") + + if filter_type == "xpath" and not operations.get("supports_xpath", False): + module.fail_json( + msg="filter value '%s' of type xpath is not supported on this device" % filter + ) + + filter_spec = (filter_type, filter) if filter_type else None + + if lock == "never": + execute_lock = False + elif target in operations.get("lock_datastore", []): + # lock is requested (always/if-support) and supported => lets do it + execute_lock = True + else: + # lock is requested (always/if-supported) but not supported => issue warning + module.warn("lock operation on '%s' source is not supported on this device" % target) + execute_lock = lock == "always" + + result = { + "changed": False, + "server_capabilities": capabilities.get("server_capabilities", []), + } + before = None + after = None + locked = False + try: + if module.params["backup"]: + response = get_config(module, target, filter_spec, lock=execute_lock) + before = to_text(tostring(response), errors="surrogate_then_replace").strip() + result["__backup__"] = before.strip() + if validate: + conn.validate(target) + if source: + if not module.check_mode: + conn.copy(source, target) + result["changed"] = True + elif delete: + if not module.check_mode: + conn.delete(target) + result["changed"] = True + elif confirm_commit: + if not module.check_mode: + conn.commit() + result["changed"] = True + elif config: + if module.check_mode and not supports_commit: + module.warn( + "check mode not supported as Netconf server doesn't support candidate capability" + ) + result["changed"] = True + module.exit_json(**result) + + if execute_lock: + conn.lock(target=target) + locked = True + if before is None: + before = to_text( + conn.get_config(source=target, filter=filter_spec), + errors="surrogate_then_replace", + ).strip() + + if format != "text": + # check for format of type json/xml/xpath + try: + config_obj, config_format = validate_and_normalize_data(config, format) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + if config_format == "json": + try: + config = dict_to_xml(config_obj) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + format = "xml" + elif config_format is None: + format = "xml" + module.warn( + "The data format of content option value couldn't be identified, hence set to 'xml'" + ) + else: + format = config_format + + validate_config(module, config, format) + + kwargs = { + "config": config, + "target": target, + "default_operation": module.params["default_operation"], + "error_option": module.params["error_option"], + "format": format, + } + + conn.edit_config(**kwargs) + + if supports_commit and module.params["commit"]: + after = to_text( + conn.get_config(source="candidate", filter=filter_spec), + errors="surrogate_then_replace", + ).strip() + if not module.check_mode: + confirm_timeout = confirm if confirm > 0 else None + confirmed_commit = True if confirm_timeout else False + conn.commit(confirmed=confirmed_commit, timeout=confirm_timeout) + else: + conn.discard_changes() + + if after is None: + after = to_text( + conn.get_config(source="running", filter=filter_spec), + errors="surrogate_then_replace", + ).strip() + + sanitized_before = sanitize_xml(before) + sanitized_after = sanitize_xml(after) + if sanitized_before != sanitized_after: + result["changed"] = True + + if result["changed"]: + if save and not module.check_mode: + conn.copy_config(target, "startup") + if module._diff: + result["diff"] = { + "before": sanitized_before, + "after": sanitized_after, + } + + except ConnectionError as e: + module.fail_json(msg=to_text(e, errors="surrogate_then_replace").strip()) + finally: + if locked: + conn.unlock(target=target) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_get.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_get.py new file mode 100644 index 0000000..88913e9 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_get.py @@ -0,0 +1,415 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, Ansible by 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 = """ +module: netconf_get +author: + - Ganesh Nalawade (@ganeshrn) + - Sven Wisotzky (@wisotzky) +short_description: Fetch configuration/state data from NETCONF enabled network devices. +description: + - NETCONF is a network management protocol developed and standardized by the IETF. + It is documented in RFC 6241. + - This module allows the user to fetch configuration and state data from NETCONF enabled + network devices. +version_added: 1.0.0 +extends_documentation_fragment: + - ansible.netcommon.network_agnostic +options: + source: + description: + - This argument specifies the datastore from which configuration data should be + fetched. Valid values are I(running), I(candidate) and I(startup). If the C(source) + value is not set both configuration and state information are returned in response + from running datastore. + type: str + choices: + - running + - candidate + - startup + filter: + description: + - This argument specifies the string which acts as a filter to restrict the + portions of the data to be are retrieved from the remote device. If this option + is not specified entire configuration or state data is returned in result depending + on the value of C(source) option. The C(filter) value can be either XML string + or XPath or JSON string or native python dictionary, if the filter is in XPath + format the NETCONF server running on remote host should support xpath capability + else it will result in an error. If the filter is in JSON format the xmltodict library + should be installed on the control node for JSON to XML conversion. + type: raw + display: + description: + - Encoding scheme to use when serializing output from the device. The option I(json) + will serialize the output as JSON data. If the option value is I(json) it requires + jxmlease to be installed on control node. The option I(pretty) is similar to + received XML response but is using human readable format (spaces, new lines). + The option value I(xml) is similar to received XML response but removes all + XML namespaces. + type: str + choices: + - json + - pretty + - xml + - native + lock: + description: + - Instructs the module to explicitly lock the datastore specified as C(source). + If no I(source) is defined, the I(running) datastore will be locked. By setting + the option value I(always) is will explicitly lock the datastore mentioned in + C(source) option. By setting the option value I(never) it will not lock the + C(source) datastore. The value I(if-supported) allows better interworking with + NETCONF servers, which do not support the (un)lock operation for all supported + datastores. + type: str + default: never + choices: + - never + - always + - if-supported +requirements: + - ncclient (>=v0.5.2) + - jxmlease (for display=json) + - xmltodict (for display=native) +notes: + - This module requires the NETCONF system service be enabled on the remote device + being managed. + - This module supports the use of connection=netconf +""" + +EXAMPLES = """ +- name: Get running configuration and state data + ansible.netcommon.netconf_get: + +- name: Get configuration and state data from startup datastore + ansible.netcommon.netconf_get: + source: startup + +- name: Get system configuration data from running datastore state (junos) + ansible.netcommon.netconf_get: + source: running + filter: + +- name: Get configuration and state data in JSON format + ansible.netcommon.netconf_get: + display: json + +- name: get schema list using subtree w/ namespaces + ansible.netcommon.netconf_get: + display: json + filter: + lock: never + +- name: get schema list using xpath + ansible.netcommon.netconf_get: + display: xml + filter: /netconf-state/schemas/schema + +- name: get interface configuration with filter (iosxr) + ansible.netcommon.netconf_get: + display: pretty + filter: + lock: if-supported + +- name: Get system configuration data from running datastore state (junos) + ansible.netcommon.netconf_get: + source: running + filter: + lock: if-supported + +- name: Get complete configuration data from running datastore (SROS) + ansible.netcommon.netconf_get: + source: running + filter: + +- name: Get complete state data (SROS) + ansible.netcommon.netconf_get: + filter: + +- name: "get configuration with json filter string and native output (using xmltodict)" + netconf_get: + filter: | + { + "interface-configurations": { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": null + } + } + display: native + +- name: Define the Cisco IOSXR interface filter + set_fact: + filter: + interface-configurations: + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg" + interface-configuration: null + +- name: "get configuration with native filter type using set_facts" + ansible.netcommon.netconf_get: + filter: "{{ filter }}" + display: native + register: result + +- name: "get configuration with direct native filter type" + ansible.netcommon.netconf_get: + filter: + { + "interface-configurations": + { + "@xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XR-ifmgr-cfg", + "interface-configuration": null, + }, + } + display: native + register: result + +# Make a round-trip interface description change, diff the before and after +# this demonstrates the use of the native display format and several utilities +# from the ansible.utils collection + +- name: Define the openconfig interface filter + set_fact: + filter: + interfaces: + "@xmlns": "http://openconfig.net/yang/interfaces" + interface: + name: Ethernet2 + +- name: Get the pre-change config using the filter + ansible.netcommon.netconf_get: + source: running + filter: "{{ filter }}" + display: native + register: pre + +- name: Update the description + ansible.utils.update_fact: + updates: + - path: pre.output.data.interfaces.interface.config.description + value: "Configured by ansible {{ 100 | random }}" + register: updated + +- name: Apply the new configuration + ansible.netcommon.netconf_config: + content: + config: + interfaces: "{{ updated.pre.output.data.interfaces }}" + +- name: Get the post-change config using the filter + ansible.netcommon.netconf_get: + source: running + filter: "{{ filter }}" + display: native + register: post + +- name: Show the differences between the pre and post configurations + ansible.utils.fact_diff: + before: "{{ pre.output.data|ansible.utils.to_paths }}" + after: "{{ post.output.data|ansible.utils.to_paths }}" +# TASK [Show the differences between the pre and post configurations] ******** +# --- before +# +++ after +# @@ -1,11 +1,11 @@ +# { +# - "@time-modified": "2020-10-23T12:27:17.462332477Z", +# + "@time-modified": "2020-10-23T12:27:21.744541708Z", +# "@xmlns": "urn:ietf:params:xml:ns:netconf:base:1.0", +# "interfaces.interface.aggregation.config['fallback-timeout']['#text']": "90", +# "interfaces.interface.aggregation.config['fallback-timeout']['@xmlns']": "http://arista.com/yang/openconfig/interfaces/augments", +# "interfaces.interface.aggregation.config['min-links']": "0", +# "interfaces.interface.aggregation['@xmlns']": "http://openconfig.net/yang/interfaces/aggregate", +# - "interfaces.interface.config.description": "Configured by ansible 56", +# + "interfaces.interface.config.description": "Configured by ansible 67", +# "interfaces.interface.config.enabled": "true", +# "interfaces.interface.config.mtu": "0", +# "interfaces.interface.config.name": "Ethernet2", +""" + +RETURN = """ +stdout: + description: The raw XML string containing configuration or state data + received from the underlying ncclient library. + returned: always apart from low-level errors (such as action plugin) + type: str + 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: ["...", "..."] +output: + description: + Based on the value of display option will return either the set of + transformed XML to JSON format from the RPC response with type dict + or pretty XML string response (human-readable) or response with + namespace removed from XML string. + returned: + If the display format is selected as I(json) it is returned as dict type + and the conversion is done using jxmlease python library. If the display + format is selected as I(native) it is returned as dict type and the conversion + is done using xmltodict python library. If the display format is xml or pretty + it is returned as a string apart from low-level errors (such as action plugin). + type: complex + contains: + formatted_output: + description: + - Contains formatted response received from remote host as per the value in display format. + type: str +""" +try: + from lxml.etree import tostring +except ImportError: + from xml.etree.ElementTree import tostring + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import ( + remove_namespaces, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.netconf.netconf import ( + get, + get_capabilities, + get_config, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.utils.data import ( + dict_to_xml, + validate_and_normalize_data, + xml_to_dict, +) + + +try: + import jxmlease + + HAS_JXMLEASE = True +except ImportError: + HAS_JXMLEASE = False + + +def main(): + """entry point for module execution""" + argument_spec = dict( + source=dict(choices=["running", "candidate", "startup"]), + filter=dict(type="raw"), + display=dict(choices=["json", "pretty", "xml", "native"]), + lock=dict(default="never", choices=["never", "always", "if-supported"]), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + capabilities = get_capabilities(module) + operations = capabilities["device_operations"] + + source = module.params["source"] + filter = module.params["filter"] + + try: + filter_data, filter_type = validate_and_normalize_data(filter) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + if filter_type == "xml": + filter_type = "subtree" + elif filter_type == "json": + try: + filter = dict_to_xml(filter_data) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + filter_type = "subtree" + elif filter_type == "xpath": + pass + elif filter_type is None: + if filter_data is not None: + # to maintain backward compatibility for ansible 2.9 which + # defaults to "subtree" filter type + filter_type = "subtree" + module.warn( + "The data format of filter option value couldn't be identified, hence set to 'subtree'" + ) + else: + pass + elif filter_type: + module.fail_json( + msg="Invalid filter type detected %s for filter value %s" % (filter_type, filter) + ) + + lock = module.params["lock"] + display = module.params["display"] + + if source == "candidate" and not operations.get("supports_commit", False): + module.fail_json(msg="candidate source is not supported on this device") + + if source == "startup" and not operations.get("supports_startup", False): + module.fail_json(msg="startup source is not supported on this device") + + if filter_type == "xpath" and not operations.get("supports_xpath", False): + module.fail_json( + msg="filter value '%s' of type xpath is not supported on this device" % filter + ) + + # If source is None, NETCONF operation is issued, reading config/state data + # from the running datastore. The python expression "(source or 'running')" results + # in the value of source (if not None) or the value 'running' (if source is None). + + if lock == "never": + execute_lock = False + elif (source or "running") in operations.get("lock_datastore", []): + # lock is requested (always/if-support) and supported => lets do it + execute_lock = True + else: + # lock is requested (always/if-supported) but not supported => issue warning + module.warn( + "lock operation on '%s' source is not supported on this device" % (source or "running") + ) + execute_lock = lock == "always" + + if display == "json" and not HAS_JXMLEASE: + module.fail_json( + msg="jxmlease is required to display response in json format" + "but does not appear to be installed. " + "It can be installed using `pip install jxmlease`" + ) + + filter_spec = (filter_type, filter) if filter_type else None + + if source is not None: + response = get_config(module, source, filter_spec, execute_lock) + else: + response = get(module, filter_spec, execute_lock) + + xml_resp = to_text(tostring(response)) + output = None + + if display == "xml": + output = remove_namespaces(xml_resp) + elif display == "json": + try: + output = jxmlease.parse(xml_resp) + except Exception: + raise ValueError(xml_resp) + elif display == "pretty": + output = to_text(tostring(response, pretty_print=True)) + elif display == "native": + try: + output = xml_to_dict(xml_resp) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + result = {"stdout": xml_resp, "output": output} + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_rpc.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_rpc.py new file mode 100644 index 0000000..e180f30 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/netconf_rpc.py @@ -0,0 +1,287 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, Ansible by 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 = """ +module: netconf_rpc +author: +- Ganesh Nalawade (@ganeshrn) +- Sven Wisotzky (@wisotzky) +short_description: Execute operations on NETCONF enabled network devices. +description: +- NETCONF is a network management protocol developed and standardized by the IETF. + It is documented in RFC 6241. +- This module allows the user to execute NETCONF RPC requests as defined by IETF RFC + standards as well as proprietary requests. +version_added: 1.0.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + rpc: + description: + - This argument specifies the request (name of the operation) to be executed on + the remote NETCONF enabled device. + required: true + type: str + xmlns: + description: + - NETCONF operations not defined in rfc6241 typically require the appropriate + XML namespace to be set. In the case the I(request) option is not already provided + in XML format, the namespace can be defined by the I(xmlns) option. + type: str + content: + description: + - This argument specifies the optional request content (all RPC attributes). The + I(content) value can either be provided as XML formatted string or as dictionary. + type: str + display: + description: + - Encoding scheme to use when serializing output from the device. The option I(json) + will serialize the output as JSON data. If the option value is I(json) it requires + jxmlease to be installed on control node. The option I(pretty) is similar to + received XML response but is using human readable format (spaces, new lines). + The option value I(xml) is similar to received XML response but removes all + XML namespaces. + type: str + choices: + - json + - pretty + - xml +requirements: +- ncclient (>=v0.5.2) +- jxmlease +notes: +- This module requires the NETCONF system service be enabled on the remote device + being managed. +- This module supports the use of connection=netconf +- To execute C(get-config), C(get) or C(edit-config) requests it is recommended to + use the Ansible I(netconf_get) and I(netconf_config) modules. +""" + +EXAMPLES = """ +- name: lock candidate + ansible.netcommon.netconf_rpc: + rpc: lock + content: + target: + candidate: + +- name: unlock candidate + ansible.netcommon.netconf_rpc: + rpc: unlock + xmlns: urn:ietf:params:xml:ns:netconf:base:1.0 + content: "{'target': {'candidate': None}}" + +- name: discard changes + ansible.netcommon.netconf_rpc: + rpc: discard-changes + +- name: get-schema + ansible.netcommon.netconf_rpc: + rpc: get-schema + xmlns: urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring + content: + identifier: ietf-netconf + version: '2011-06-01' + +- name: copy running to startup + ansible.netcommon.netconf_rpc: + rpc: copy-config + content: + source: + running: + target: + startup: + +- name: get schema list with JSON output + ansible.netcommon.netconf_rpc: + rpc: get + content: | + + + + + + display: json + +- name: get schema using XML request + ansible.netcommon.netconf_rpc: + rpc: get-schema + xmlns: urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring + content: | + ietf-netconf-monitoring + 2010-10-04 + display: json +""" + +RETURN = """ +stdout: + description: The raw XML string containing configuration or state data + received from the underlying ncclient library. + returned: always apart from low-level errors (such as action plugin) + type: str + 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: ['...', '...'] +output: + description: Based on the value of display option will return either the set of + transformed XML to JSON format from the RPC response with type dict + or pretty XML string response (human-readable) or response with + namespace removed from XML string. + returned: when the display format is selected as JSON it is returned as dict type, if the + display format is xml or pretty pretty it is returned as a string apart from low-level + errors (such as action plugin). + type: complex + contains: + formatted_output: + description: + - Contains formatted response received from remote host as per the value in display format. + type: str +""" + +import ast + + +try: + from lxml.etree import tostring +except ImportError: + from xml.etree.ElementTree import tostring + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.netconf import ( + remove_namespaces, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.netconf.netconf import ( + dispatch, +) + + +try: + import jxmlease + + HAS_JXMLEASE = True +except ImportError: + HAS_JXMLEASE = False + + +def get_xml_request(module, request, xmlns, content): + if content is None: + if xmlns is None: + return "<%s/>" % request + else: + return '<%s xmlns="%s"/>' % (request, xmlns) + + if isinstance(content, str): + content = content.strip() + + if content.startswith("<") and content.endswith(">"): + # assumption content contains already XML payload + if xmlns is None: + return "<%s>%s" % (request, content, request) + else: + return '<%s xmlns="%s">%s' % ( + request, + xmlns, + content, + request, + ) + + try: + # trying if content contains dict + content = ast.literal_eval(content) + except Exception: + module.fail_json(msg="unsupported content value `%s`" % content) + + if isinstance(content, dict): + if not HAS_JXMLEASE: + module.fail_json( + msg="jxmlease is required to convert RPC content to XML " + "but does not appear to be installed. " + "It can be installed using `pip install jxmlease`" + ) + + payload = jxmlease.XMLDictNode(content).emit_xml(pretty=False, full_document=False) + if xmlns is None: + return "<%s>%s" % (request, payload, request) + else: + return '<%s xmlns="%s">%s' % ( + request, + xmlns, + payload, + request, + ) + + module.fail_json(msg="unsupported content data-type `%s`" % type(content).__name__) + + +def main(): + """entry point for module execution""" + argument_spec = dict( + rpc=dict(type="str", required=True), + xmlns=dict(type="str"), + content=dict(), + display=dict(choices=["json", "pretty", "xml"]), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + rpc = module.params["rpc"] + xmlns = module.params["xmlns"] + content = module.params["content"] + display = module.params["display"] + + if rpc is None: + module.fail_json(msg="argument `rpc` must not be None") + + rpc = rpc.strip() + if len(rpc) == 0: + module.fail_json(msg="argument `rpc` must not be empty") + + if rpc in ["close-session"]: + # explicit close-session is not allowed, as this would make the next + # NETCONF operation to the same host fail + module.fail_json(msg="unsupported operation `%s`" % rpc) + + if display == "json" and not HAS_JXMLEASE: + module.fail_json( + msg="jxmlease is required to display response in json format" + "but does not appear to be installed. " + "It can be installed using `pip install jxmlease`" + ) + + xml_req = get_xml_request(module, rpc, xmlns, content) + response = dispatch(module, xml_req) + + xml_resp = tostring(response) + output = None + + if display == "xml": + output = remove_namespaces(xml_resp) + elif display == "json": + try: + output = jxmlease.parse(xml_resp) + except Exception: + raise ValueError(xml_resp) + elif display == "pretty": + output = tostring(response, pretty_print=True) + + result = {"stdout": xml_resp, "output": output} + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/network_resource.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/network_resource.py new file mode 100644 index 0000000..1401406 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/network_resource.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# 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: network_resource +author: Ganesh B. Nalawade (@ganeshrn) +short_description: Manage resource modules +description: +- Get list of available resource modules for given os name +- Retrieve given resource module configuration facts +- Push given resource module configuration +options: + os_name: + type: str + description: + - The name of the os to manage the resource modules. + - The name should be fully qualified collection name format, + that is I(..). + - If value of this option is not set the os value will be + read from I(ansible_network_os) variable. + - If value of both I(os_name) and I(ansible_network_os) + is not set it will result in error. + name: + type: str + description: + - The name of the resource module to manage. + - The resource module should be supported for given I(os_name), + if not supported it will result in error. + config: + description: + - The resource module configuration. For details on the type and + structure of this option refer the individual resource module + platform documentation. + type: raw + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the host device + by executing the cli command to get the resource configuration on host. + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + state: + description: + - The state the configuration should be left in. + - For supported values refer the individual resource module + platform documentation. +version_added: 2.4.0 +notes: +- Refer the individual module documentation for the valid inputs of I(state) + and I(config) modules. +""" + +EXAMPLES = """ +- name: get list of resource modules for given network_os + ansible.netcommon.network_resource: + register: result + +- name: fetch acl config for + ansible.netcommon.network_resource: + os_name: cisco.ios.ios + name: acls + state: gathered + +- name: manage acl config for cisco.ios.ios network os. + ansible.netcommon.network_resource: + name: acls + config: + - afi: ipv4 + acls: + - name: test_acl + acl_type: extended + aces: + - grant: deny + protocol_options: + tcp: + fin: true + source: + address: 192.0.2.0 + wildcard_bits: 0.0.0.255 + destination: + address: 192.0.3.0 + wildcard_bits: 0.0.0.255 + port_protocol: + eq: www + option: + traceroute: true + ttl: + eq: 10 + state: merged +""" + +RETURN = """ +modules: + description: List of resource modules supported for given OS. + returned: When only I(os_name) or I(ansible_network_os) is set + type: list + sample: ["acl_interfaces", "acls", "bgp_global"] +before: + description: The configuration as structured data prior to module invocation. + returned: When I(state) and/or I(config) option is set + type: list + sample: The configuration returned will always be in the same format of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed and when I(state) and/or I(config) option is set + type: list + sample: The configuration returned will always be in the same format of the parameters above. +commands: + description: The set of commands pushed to the remote device + returned: When I(state) and/or I(config) option is set + type: list + sample: ['ip access-list extended 110', ] +""" diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/restconf_config.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/restconf_config.py new file mode 100644 index 0000000..2adca7a --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/restconf_config.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# Copyright: 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: restconf_config +author: Ganesh Nalawade (@ganeshrn) +short_description: Handles create, update, read and delete of configuration data on + RESTCONF enabled devices. +description: +- RESTCONF is a standard mechanisms to allow web applications to configure and manage + data. RESTCONF is a IETF standard and documented on RFC 8040. +- This module allows the user to configure data on RESTCONF enabled devices. +version_added: 1.0.0 +options: + path: + description: + - URI being used to execute API calls. + required: true + type: str + content: + description: + - The configuration data in format as specififed in C(format) option. Required + unless C(method) is I(delete). + type: str + method: + description: + - The RESTCONF method to manage the configuration change on device. The value + I(post) is used to create a data resource or invoke an operation resource, I(put) + is used to replace the target data resource, I(patch) is used to modify the + target resource, and I(delete) is used to delete the target resource. + required: false + type: str + default: post + choices: + - post + - put + - patch + - delete + format: + description: + - The format of the configuration provided as value of C(content). Accepted values + are I(xml) and I(json) and the given configuration format should be supported + by remote RESTCONF server. + type: str + default: json + choices: + - json + - xml +notes: +- This module requires the RESTCONF system service be enabled on the remote device + being managed. +- This module is supported with I(ansible_connection) value of I(ansible.netcommon.httpapi) and + I(ansible_network_os) value of I(ansible.netcommon.restconf). +- This module is tested against Cisco IOSXE 16.12.02 version. +""" + +EXAMPLES = """ +- name: create l3vpn services + ansible.netcommon.restconf_config: + path: /config/ietf-l3vpn-svc:l3vpn-svc/vpn-services + content: | + { + "vpn-service":[ + { + "vpn-id": "red_vpn2", + "customer-name": "blue", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + }, + { + "vpn-id": "blue_vpn1", + "customer-name": "red", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + } +""" + +RETURN = """ +candidate: + description: The configuration sent to the device. + returned: When the method is not delete + type: dict + sample: | + { + "vpn-service": [ + { + "customer-name": "red", + "vpn-id": "blue_vpn1", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + } +running: + description: The current running configuration on the device. + returned: When the method is not delete + type: dict + sample: | + { + "vpn-service": [ + { + "vpn-id": "red_vpn2", + "customer-name": "blue", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + }, + { + "vpn-id": "blue_vpn1", + "customer-name": "red", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + } + +""" + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import ConnectionError + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_diff, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.restconf import restconf + + +string_types = (str,) + + +def main(): + """entry point for module execution""" + argument_spec = dict( + path=dict(required=True), + content=dict(), + method=dict(choices=["post", "put", "patch", "delete"], default="post"), + format=dict(choices=["json", "xml"], default="json"), + ) + required_if = [ + ["method", "post", ["content"]], + ["method", "put", ["content"]], + ["method", "patch", ["content"]], + ] + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True, + ) + + path = module.params["path"] + candidate = module.params["content"] + method = module.params["method"] + format = module.params["format"] + + if isinstance(candidate, string_types): + candidate = json.loads(candidate) + + warnings = list() + result = {"changed": False, "warnings": warnings} + + running = None + commit = not module.check_mode + try: + running = restconf.get(module, path, output=format) + except ConnectionError as exc: + if exc.code == 404: + running = None + else: + module.fail_json(msg=to_text(exc), code=exc.code) + + try: + if method == "delete": + if running: + if commit: + restconf.edit_config(module, path=path, method="DELETE") + result["changed"] = True + else: + warnings.append("delete not executed as resource '%s' does not exist" % path) + else: + if running: + diff = dict_diff(running, candidate) + result["candidate"] = candidate + result["running"] = running + else: + diff = candidate + + if diff: + if module._diff: + result["diff"] = { + "prepared": diff, + "before": candidate, + "after": running, + } + + if commit: + restconf.edit_config( + module, + path=path, + content=diff, + method=method.upper(), + format=format, + ) + result["changed"] = True + + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/restconf_get.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/restconf_get.py new file mode 100644 index 0000000..8a7efbc --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/restconf_get.py @@ -0,0 +1,123 @@ +#!/usr/bin/python +# Copyright: 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: restconf_get +author: Ganesh Nalawade (@ganeshrn) +short_description: Fetch configuration/state data from RESTCONF enabled devices. +description: +- RESTCONF is a standard mechanisms to allow web applications to access the configuration + data and state data developed and standardized by the IETF. It is documented in + RFC 8040. +- This module allows the user to fetch configuration and state data from RESTCONF + enabled devices. +version_added: 1.0.0 +options: + path: + description: + - URI being used to execute API calls. + required: true + type: str + content: + description: + - The C(content) is a query parameter that controls how descendant nodes of the + requested data nodes in C(path) will be processed in the reply. If value is + I(config) return only configuration descendant data nodes of value in C(path). + If value is I(nonconfig) return only non-configuration descendant data nodes + of value in C(path). If value is I(all) return all descendant data nodes of + value in C(path) + type: str + required: false + choices: + - config + - nonconfig + - all + output: + description: + - The output of response received. + required: false + type: str + default: json + choices: + - json + - xml +notes: +- This module requires the RESTCONF system service be enabled on the remote device + being managed. +- This module is supported with I(ansible_connection) value of I(ansible.netcommon.httpapi) and + I(ansible_network_os) value of I(ansible.netcommon.restconf). +- This module is tested against Cisco IOSXE 16.12.02 version. +""" + +EXAMPLES = """ +- name: get l3vpn services + ansible.netcommon.restconf_get: + path: /config/ietf-l3vpn-svc:l3vpn-svc/vpn-services +""" + +RETURN = """ +response: + description: A dictionary representing a JSON-formatted response + returned: when the device response is valid JSON + type: dict + sample: | + { + "vpn-services": { + "vpn-service": [ + { + "customer-name": "red", + "vpn-id": "blue_vpn1", + "vpn-service-topology": "ietf-l3vpn-svc:any-to-any" + } + ] + } + } + +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.common.text.converters import to_text +from ansible.module_utils.connection import ConnectionError + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.restconf import restconf +from ansible_collections.ansible.netcommon.plugins.module_utils.utils.data import xml_to_dict + + +def main(): + """entry point for module execution""" + argument_spec = dict( + path=dict(required=True), + content=dict(choices=["config", "nonconfig", "all"]), + output=dict(choices=["json", "xml"], default="json"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + result = {"changed": False} + + try: + response = restconf.get(module, **module.params) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc), code=exc.code) + + if module.params["output"] == "xml": + try: + response = xml_to_dict(response) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + result.update({"response": response}) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/modules/telnet.py b/collections/ansible_collections/ansible/netcommon/plugins/modules/telnet.py new file mode 100644 index 0000000..41abf23 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/modules/telnet.py @@ -0,0 +1,130 @@ +# this is a virtual module that is entirely implemented server side +# Copyright: 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: telnet +short_description: Executes a low-down and dirty telnet command +description: + - Executes a low-down and dirty telnet command, not going through the module subsystem. + - This is mostly to be used for enabling ssh on devices that only have telnet enabled + by default. +version_added: 1.0.0 +options: + command: + description: + - List of commands to be executed in the telnet session. + required: true + type: list + elements: str + aliases: + - commands + host: + description: + - The host/target on which to execute the command + required: false + type: str + default: remote_addr + user: + description: + - The user for login + required: false + type: str + default: remote_user + password: + description: + - The password for login + type: str + port: + description: + - Remote port to use + type: int + default: 23 + timeout: + description: + - timeout for remote operations + type: int + default: 120 + prompts: + description: + - List of prompts expected before sending next command + required: false + type: list + elements: str + default: + - $ + login_prompt: + description: + - Login or username prompt to expect + required: false + type: str + default: "login: " + password_prompt: + description: + - Login or username prompt to expect + required: false + type: str + default: "Password: " + pause: + description: + - Seconds to pause between each command issued + required: false + type: int + default: 1 + send_newline: + description: + - Sends a newline character upon successful connection to start the terminal session. + required: false + type: bool + default: false + crlf: + description: + - Sends a CRLF (Carrage Return) instead of just a LF (Line Feed). + required: false + type: bool + default: false +notes: + - The C(environment) keyword does not work with this task +author: + - Ansible Core Team +""" + +EXAMPLES = """ +- name: send configuration commands to IOS + ansible.netcommon.telnet: + user: cisco + password: cisco + login_prompt: "Username: " + prompts: + - "[>#]" + command: + - terminal length 0 + - configure terminal + - hostname ios01 + +- name: run show commands + ansible.netcommon.telnet: + user: cisco + password: cisco + login_prompt: "Username: " + prompts: + - "[>#]" + command: + - terminal length 0 + - show version +""" + +RETURN = """ +output: + description: output of each command is an element in this list + type: list + returned: always + sample: ["success", "success", "", "warning .. something"] +""" diff --git a/collections/ansible_collections/ansible/netcommon/plugins/netconf/default.py b/collections/ansible_collections/ansible/netcommon/plugins/netconf/default.py new file mode 100644 index 0000000..30fdfc0 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/netconf/default.py @@ -0,0 +1,58 @@ +# +# (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: + - Ansible Networking Team (@ansible-network) +name: default +short_description: Use default netconf plugin to run standard netconf commands as + per RFC +description: +- This default plugin provides low level abstraction apis for sending and receiving + netconf commands as per Netconf RFC specification. +version_added: 1.0.0 +options: + ncclient_device_handler: + type: str + default: default + description: + - Specifies the ncclient device handler name for network os that support default + netconf implementation as per Netconf RFC specification. To identify the ncclient + device handler name refer ncclient library documentation. +""" +import json + +from ansible.module_utils.common.text.converters import to_text + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.netconf_base import NetconfBase + + +class Netconf(NetconfBase): + def get_text(self, ele, tag): + try: + return to_text(ele.find(tag).text, errors="surrogate_then_replace").strip() + except AttributeError: + pass + + def get_device_info(self): + device_info = dict() + device_info["network_os"] = "default" + return device_info + + def get_capabilities(self): + result = dict() + result["rpc"] = self.get_base_rpc() + result["network_api"] = "netconf" + result["device_info"] = self.get_device_info() + result["server_capabilities"] = list(self.m.server_capabilities) + result["client_capabilities"] = list(self.m.client_capabilities) + result["session_id"] = self.m.session_id + result["device_operations"] = self.get_device_operations(result["server_capabilities"]) + return json.dumps(result) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/argspec_filter_utils.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/argspec_filter_utils.py new file mode 100644 index 0000000..d0a8a8f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/argspec_filter_utils.py @@ -0,0 +1,61 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +""" +Shared utilities for filter plugins that use AnsibleArgSpecValidator. + +In Ansible 2.19+, filter arguments may be wrapped in lazy containers from +ansible-core (lib/ansible/_internal/_templating/_lazy_containers.py). Those +containers subclass dict/list/tuple and resolve values on iteration (e.g. +``.items()``, ``__iter__``). They implement ``__deepcopy__`` but it deepcopies +``_templar`` and ``_lazy_options``, which can fail; ansible-core's +ArgumentSpecValidator (lib/ansible/module_utils/common/arg_spec.py) uses +deepcopy in ValidationResult, so passing lazy containers can raise. Converting +filter args to native types before AnsibleArgSpecValidator avoids that. This +implementation aligns with the lazy containers: we recurse over dict/list/tuple +(so lazy subclasses are iterated and thus resolved) and build plain native +containers. ansible-core does not currently expose a public API for "lazy to +native" conversion; the only mechanism is the private ``_non_lazy_copy()`` on +lazy container instances (ansible._internal._templating._lazy_containers). We +avoid importing from ``_internal`` so collections remain compatible with +future core changes; if core adds a public API later, this helper can be +updated to use it. +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +def convert_to_native(value): + """Convert Ansible lazy containers and wrapped types to native Python types. + + Use this for filter plugin argument dicts before passing them to + AnsibleArgSpecValidator. Lazy containers (see ansible-core + ``_lazy_containers.py``) subclass dict/list/tuple and resolve on iteration; + this function iterates and recurses so resolved values become plain dicts, + lists, and scalars, avoiding deepcopy failures in ArgumentSpecValidator. + + Recommendation: keep this implementation and avoid importing from + ansible._internal (e.g. _non_lazy_copy()). If ansible-core adds a public + "lazy to native" API later, this helper can be updated to use it. + """ + import json + + if value is None: + return None + if isinstance(value, (bool, int, float, str)): + return value + if isinstance(value, (list, tuple)): + return [convert_to_native(item) for item in value] + if isinstance(value, dict): + return {convert_to_native(k): convert_to_native(v) for k, v in value.items()} + # For any other type, try to convert via JSON round-trip to get native types + try: + return json.loads(json.dumps(value)) + except (TypeError, ValueError): + return value diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/cliconf_base.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/cliconf_base.py new file mode 100644 index 0000000..8a78207 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/cliconf_base.py @@ -0,0 +1,564 @@ +# +# (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 + +from abc import abstractmethod +from collections.abc import Mapping +from functools import wraps + +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.module_utils.common.text.converters import to_bytes, to_text + +# Needed to satisfy PluginLoader's required_base_class +from ansible.plugins.cliconf import CliconfBase as CliconfBaseBase + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list + + +try: + from scp import SCPClient + + HAS_SCP = True +except ImportError: + HAS_SCP = False + + +def enable_mode(func): + @wraps(func) + def wrapped(self, *args, **kwargs): + prompt = self._connection.get_prompt() + if not to_text(prompt, errors="surrogate_or_strict").strip().endswith("#"): + raise AnsibleError("operation requires privilege escalation") + return func(self, *args, **kwargs) + + return wrapped + + +class CliconfBase(CliconfBaseBase): + """ + A base class for implementing cli connections + + .. note:: String inputs to :meth:`send_command` will be cast to byte strings + within this method and as such are not required to be made byte strings + beforehand. Please avoid using literal byte strings (``b'string'``) in + :class:`CliConfBase` plugins as this can lead to unexpected errors when + running on Python 3 + + List of supported rpc's: + :get_config: Retrieves the specified configuration from the device + :edit_config: Loads the specified commands into the remote device + :get: Execute specified command on remote device + :get_capabilities: Retrieves device information and supported rpc methods + :commit: Load configuration from candidate to running + :discard_changes: Discard changes to candidate datastore + + Note: List of supported rpc's for remote device can be extracted from + output of get_capabilities() + + :returns: Returns output received from remote device as byte string + + Usage: + from ansible.module_utils.connection import Connection + + conn = Connection() + conn.get('show lldp neighbors detail'') + conn.get_config('running') + conn.edit_config(['hostname test', 'netconf ssh']) + """ + + __rpc__ = [ + "edit_config", + "enable_response_logging", + "get", + "get_capabilities", + "get_config", + "disable_response_logging", + "run_commands", + ] + + def __init__(self, connection): + super(CliconfBase, self).__init__(connection) + self._connection = connection + self.history = list() + self.response_logging = False + + def _alarm_handler(self, signum, frame): + """Alarm handler raised in case of command timeout""" + self._connection.queue_message( + "log", + "closing shell due to command timeout (%s seconds)." + % self._connection._play_context.timeout, + ) + self.close() + + def send_command( + self, + command=None, + prompt=None, + answer=None, + sendonly=False, + newline=True, + prompt_retry_check=False, + check_all=False, + strip_prompt=True, + ): + """Executes a command over the device connection + + This method will execute a command over the device connection and + return the results to the caller. This method will also perform + logging of any commands based on the `nolog` argument. + + :param command: The command to send over the connection to the device + :param prompt: A single regex pattern or a sequence of patterns to evaluate the expected prompt from the command + :param answer: The answer to respond with if the prompt is matched. + :param sendonly: Bool value that will send the command but not wait for a result. + :param newline: Bool value that will append the newline character to the command + :param prompt_retry_check: Bool value for trying to detect more prompts + :param check_all: Bool value to indicate if all the values in prompt sequence should be matched or any one of + given prompt. + :param strip_prompt: Bool value to indicate if the matched prompt is removed from the returned response or not. + :returns: The output from the device after executing the command + """ + kwargs = { + "command": to_bytes(command), + "sendonly": sendonly, + "newline": newline, + "prompt_retry_check": prompt_retry_check, + "check_all": check_all, + "strip_prompt": strip_prompt, + } + + if prompt is not None: + if isinstance(prompt, list): + kwargs["prompt"] = [to_bytes(p) for p in prompt] + else: + kwargs["prompt"] = to_bytes(prompt) + if answer is not None: + if isinstance(answer, list): + kwargs["answer"] = [to_bytes(p) for p in answer] + else: + kwargs["answer"] = to_bytes(answer) + + resp = self._connection.send(**kwargs) + + if not self.response_logging: + self.history.append(("*****", "*****")) + else: + self.history.append((kwargs["command"], resp)) + + return resp + + def get_base_rpc(self): + """Returns list of base rpc method supported by remote device""" + return self.__rpc__ + + def get_history(self): + """Returns the history file for all commands + + This will return a log of all the commands that have been sent to + the device and all of the output received. By default, all commands + and output will be redacted unless explicitly configured otherwise. + + :return: An ordered list of command, output pairs + """ + return self.history + + def reset_history(self): + """Resets the history of run commands + :return: None + """ + self.history = list() + + def enable_response_logging(self): + """Enable logging command response""" + self.response_logging = True + + def disable_response_logging(self): + """Disable logging command response""" + self.response_logging = False + + @abstractmethod + def get_config(self, source="running", flags=None, format=None): + """Retrieves the specified configuration from the device + + This method will retrieve the configuration specified by source and + return it to the caller as a string. Subsequent calls to this method + will retrieve a new configuration from the device + + :param source: The configuration source to return from the device. + This argument accepts either `running` or `startup` as valid values. + + :param flags: For devices that support configuration filtering, this + keyword argument is used to filter the returned configuration. + The use of this keyword argument is device dependent adn will be + silently ignored on devices that do not support it. + + :param format: For devices that support fetching different configuration + format, this keyword argument is used to specify the format in which + configuration is to be retrieved. + + :return: The device configuration as specified by the source argument. + """ + pass + + @abstractmethod + def edit_config( + self, + candidate=None, + commit=True, + replace=None, + diff=False, + comment=None, + ): + """Loads the candidate configuration into the network device + + This method will load the specified candidate config into the device + and merge with the current configuration unless replace is set to + True. If the device does not support config replace an errors + is returned. + + :param candidate: The configuration to load into the device and merge + with the current running configuration + + :param commit: Boolean value that indicates if the device candidate + configuration should be pushed in the running configuration or discarded. + + :param replace: If the value is True/False it indicates if running configuration should be completely + replace by candidate configuration. It can also take configuration file path as value, + the file in this case should be present on the remote host in the mentioned path as a + prerequisite. + :param comment: Commit comment provided it is supported by remote host. + :return: Returns a json string with contains configuration applied on remote host, the returned + response on executing configuration commands and platform relevant data. + { + "diff": "", + "response": [], + "request": [] + } + + """ + pass + + def get( + self, + command=None, + prompt=None, + answer=None, + sendonly=False, + newline=True, + output=None, + check_all=False, + ): + """Execute specified command on remote device + This method will retrieve the specified data and + return it to the caller as a string. + :param command: command in string format to be executed on remote device + :param prompt: the expected prompt generated by executing command, this can + be a string or a list of strings + :param answer: the string to respond to the prompt with + :param sendonly: bool to disable waiting for response, default is false + :param newline: bool to indicate if newline should be added at end of answer or not + :param output: For devices that support fetching command output in different + format, this keyword argument is used to specify the output in which + response is to be retrieved. + :param check_all: Bool value to indicate if all the values in prompt sequence should be matched or any one of + given prompt. + :return: The output from the device after executing the command + """ + if not command: + raise ValueError("must provide value of command to execute") + + return self.send_command( + command=command, + prompt=prompt, + answer=answer, + sendonly=sendonly, + newline=newline, + check_all=check_all, + ) + + @abstractmethod + def get_capabilities(self): + """Returns the basic capabilities of the network device + This method will provide some basic facts about the device and + what capabilities it has to modify the configuration. The minimum + return from this method takes the following format. + eg: + { + + 'rpc': [list of supported rpcs], + 'network_api': , # the name of the transport + 'device_info': { + 'network_os': , + 'network_os_version': , + 'network_os_model': , + 'network_os_hostname': , + 'network_os_image': , + 'network_os_platform': , + }, + 'device_operations': { + 'supports_diff_replace': , # identify if config should be merged or replaced is supported + 'supports_commit': , # identify if commit is supported by device or not + 'supports_rollback': , # identify if rollback is supported or not + 'supports_defaults': , # identify if fetching running config with default is supported + 'supports_commit_comment': , # identify if adding comment to commit is supported of not + 'supports_onbox_diff: , # identify if on box diff capability is supported or not + 'supports_generate_diff: , # identify if diff capability is supported within plugin + 'supports_multiline_delimiter: , # identify if multiline demiliter is supported within config + 'supports_diff_match: , # identify if match is supported + 'supports_diff_ignore_lines: , # identify if ignore line in diff is supported + 'supports_config_replace': , # identify if running config replace with candidate config is supported + 'supports_admin': , # identify if admin configure mode is supported or not + 'supports_commit_label': , # identify if commit label is supported or not + } + 'format': [list of supported configuration format], + 'diff_match': [list of supported match values], + 'diff_replace': [list of supported replace values], + 'output': [list of supported command output format] + } + :return: capability as json string + """ + result = {} + result["rpc"] = self.get_base_rpc() + result["device_info"] = self.get_device_info() + result["network_api"] = "cliconf" + return result + + @abstractmethod + def get_device_info(self): + """Returns basic information about the network device. + + This method will provide basic information about the device such as OS version and model + name. This data is expected to be used to fill the 'device_info' key in get_capabilities() + above. + + :return: dictionary of device information + """ + pass + + def commit(self, comment=None): + """Commit configuration changes + + This method will perform the commit operation on a previously loaded + candidate configuration that was loaded using `edit_config()`. If + there is a candidate configuration, it will be committed to the + active configuration. If there is not a candidate configuration, this + method should just silently return. + + :return: None + """ + raise AnsibleConnectionFailure( + "commit is not supported by network_os %s" % self._play_context.network_os + ) + + def discard_changes(self): + """Discard candidate configuration + + This method will discard the current candidate configuration if one + is present. If there is no candidate configuration currently loaded, + then this method should just silently return + + :returns: None + """ + raise AnsibleConnectionFailure( + "discard_changes is not supported by network_os %s" % self._play_context.network_os + ) + + def rollback(self, rollback_id, commit=True): + """ + + :param rollback_id: The commit id to which configuration should be rollbacked + :param commit: Flag to indicate if changes should be committed or not + :return: Returns diff between before and after change. + """ + pass + + def copy_file(self, source=None, destination=None, proto="scp", timeout=30): + """Copies file over scp/sftp to remote device + + :param source: Source file path + :param destination: Destination file path on remote device + :param proto: Protocol to be used for file transfer, + supported protocol: scp and sftp + :param timeout: Specifies the wait time to receive response from + remote host before triggering timeout exception + :return: None + """ + ssh = self._connection.paramiko_conn._connect_uncached() + if proto == "scp": + if not HAS_SCP: + raise AnsibleError( + "Required library scp is not installed. Please install it using `pip install scp`" + ) + with SCPClient(ssh.get_transport(), socket_timeout=timeout) as scp: + scp.put(source, destination) + elif proto == "sftp": + with ssh.open_sftp() as sftp: + sftp.put(source, destination) + + def get_file(self, source=None, destination=None, proto="scp", timeout=30): + """Fetch file over scp/sftp from remote device + :param source: Source file path + :param destination: Destination file path + :param proto: Protocol to be used for file transfer, + supported protocol: scp and sftp + :param timeout: Specifies the wait time to receive response from + remote host before triggering timeout exception + :return: None + """ + """Fetch file over scp/sftp from remote device""" + ssh = self._connection.paramiko_conn._connect_uncached() + if proto == "scp": + if not HAS_SCP: + raise AnsibleError( + "Required library scp is not installed. Please install it using `pip install scp`" + ) + try: + with SCPClient(ssh.get_transport(), socket_timeout=timeout) as scp: + scp.get(source, destination) + except EOFError: + # This appears to be benign. + pass + elif proto == "sftp": + with ssh.open_sftp() as sftp: + sftp.get(source, destination) + + def get_diff( + self, + candidate=None, + running=None, + diff_match=None, + diff_ignore_lines=None, + path=None, + diff_replace=None, + ): + """ + Generate diff between candidate and running configuration. If the + remote host supports onbox diff capabilities ie. supports_onbox_diff in that case + candidate and running configurations are not required to be passed as argument. + In case if onbox diff capability is not supported candidate argument is mandatory + and running argument is optional. + :param candidate: The configuration which is expected to be present on remote host. + :param running: The base configuration which is used to generate diff. + :param diff_match: Instructs how to match the candidate configuration with current device configuration + Valid values are 'line', 'strict', 'exact', 'none'. + 'line' - commands are matched line by line + 'strict' - command lines are matched with respect to position + 'exact' - command lines must be an equal match + 'none' - will not compare the candidate configuration with the running configuration + :param diff_ignore_lines: Use this argument to specify one or more lines that should be + ignored during the diff. This is used for lines in the configuration + that are automatically updated by the system. This argument takes + a list of regular expressions or exact line matches. + :param path: The ordered set of parents that uniquely identify the section or hierarchy + the commands should be checked against. If the parents argument + is omitted, the commands are checked against the set of top + level or global commands. + :param diff_replace: Instructs on the way to perform the configuration on the device. + If the replace argument is set to I(line) then the modified lines are + pushed to the device in configuration mode. If the replace argument is + set to I(block) then the entire command block is pushed to the device in + configuration mode if any line is not correct. + :return: Configuration and/or banner diff in json format. + { + 'config_diff': '' + } + + """ + pass + + def run_commands(self, commands=None, check_rc=True): + """ + Execute a list of commands on remote host and return the list of response + :param commands: The list of command that needs to be executed on remote host. + The individual command in list can either be a command string or command dict. + If the command is dict the valid keys are + { + 'command': + 'prompt': , + 'answer': , + 'output': , + 'sendonly': + } + :param check_rc: Boolean flag to check if returned response should be checked for error or not. + If check_rc is False the error output is appended in return response list, else if the + value is True an exception is raised. + :return: List of returned response + """ + if commands is None: + raise ValueError("'commands' value is required") + + responses = list() + for cmd in to_list(commands): + if not isinstance(cmd, Mapping): + cmd = {"command": cmd} + + try: + out = self.send_command(**cmd) + except AnsibleConnectionFailure as e: + if check_rc: + raise + out = getattr(e, "err", e) + + responses.append(out) + + return responses + + def check_edit_config_capability( + self, + operations, + candidate=None, + commit=True, + replace=None, + comment=None, + ): + if not candidate and not replace: + raise ValueError("must provide a candidate or replace to load configuration") + + if commit not in (True, False): + raise ValueError("'commit' must be a bool, got %s" % commit) + + if replace and not operations["supports_replace"]: + raise ValueError("configuration replace is not supported") + + if comment and not operations.get("supports_commit_comment", False): + raise ValueError("commit comment is not supported") + + if replace and not operations.get("supports_replace", False): + raise ValueError("configuration replace is not supported") + + def set_cli_prompt_context(self): + """ + Ensure the command prompt on device is in right mode + :return: None + """ + pass + + def _update_cli_prompt_context(self, config_context=None, exit_command="exit"): + """ + Update the cli prompt context to ensure it is in operational mode + :param config_context: It is string value to identify if the current cli prompt ends with config mode prompt + :param exit_command: Command to execute to exit the config mode + :return: None + """ + out = self._connection.get_prompt() + if out is None: + raise AnsibleConnectionFailure( + message="cli prompt is not identified from the last received" + " response window: %s" % self._connection._last_recv_window + ) + + while True: + out = to_text(out, errors="surrogate_then_replace").strip() + if config_context and out.endswith(config_context): + self._connection.queue_message("vvvv", "wrong context, sending exit to device") + self.send_command(exit_command) + out = self._connection.get_prompt() + else: + break diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/comp_type5.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/comp_type5.py new file mode 100644 index 0000000..9672e0e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/comp_type5.py @@ -0,0 +1,27 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The comp_type5 plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible_collections.ansible.netcommon.plugins.plugin_utils.hash_salt import hash_salt +from ansible_collections.ansible.netcommon.plugins.plugin_utils.type5_pw import type5_pw + + +def comp_type5(unencrypted_password, encrypted_password, return_original=False): + salt = hash_salt(encrypted_password) + if type5_pw(unencrypted_password, salt) == encrypted_password: + if return_original is True: + return encrypted_password + else: + return True + return False diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/compat/telnetlib.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/compat/telnetlib.py new file mode 100644 index 0000000..f237b54 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/compat/telnetlib.py @@ -0,0 +1,681 @@ +# Vendored copy of https://github.com/python/cpython/blob/62d55a4d11fe25e8981e27e68ba080ab47c3f590/Lib/telnetlib.py +# Python Software Foundation License 2.0 (see LICENSES/PSF-2.0.txt or https://opensource.org/licenses/Python-2.0) +# SPDX-License-Identifier: PSF-2.0 +r"""TELNET client class. + +Based on RFC 854: TELNET Protocol Specification, by J. Postel and +J. Reynolds + +Example: + +>>> from telnetlib import Telnet +>>> tn = Telnet('www.python.org', 79) # connect to finger port +>>> tn.write(b'guido\r\n') +>>> print(tn.read_all()) +Login Name TTY Idle When Where +guido Guido van Rossum pts/2 snag.cnri.reston.. + +>>> + +Note that read_all() won't read until eof -- it just reads some data +-- but it guarantees to read at least one byte unless EOF is hit. + +It is possible to pass a Telnet object to a selector in order to wait until +more data is available. Note that in this case, read_eager() may return b'' +even if there was data on the socket, because the protocol negotiation may have +eaten the data. This is why EOFError is needed in some cases to distinguish +between "no data" and "connection closed" (since the socket also appears ready +for reading when it is closed). + +To do: +- option negotiation +- timeout should be intrinsic to the connection object instead of an + option on one of the read calls only + +""" + +# Imported modules +import selectors +import socket +import sys + +from time import monotonic as _time + + +__all__ = ["Telnet"] + +# Tunable parameters +DEBUGLEVEL = 0 + +# Telnet protocol defaults +TELNET_PORT = 23 + +# Telnet protocol characters (don't change) +IAC = bytes([255]) # "Interpret As Command" +DONT = bytes([254]) +DO = bytes([253]) +WONT = bytes([252]) +WILL = bytes([251]) +theNULL = bytes([0]) + +SE = bytes([240]) # Subnegotiation End +NOP = bytes([241]) # No Operation +DM = bytes([242]) # Data Mark +BRK = bytes([243]) # Break +IP = bytes([244]) # Interrupt process +AO = bytes([245]) # Abort output +AYT = bytes([246]) # Are You There +EC = bytes([247]) # Erase Character +EL = bytes([248]) # Erase Line +GA = bytes([249]) # Go Ahead +SB = bytes([250]) # Subnegotiation Begin + + +# Telnet protocol options code (don't change) +# These ones all come from arpa/telnet.h +BINARY = bytes([0]) # 8-bit data path +ECHO = bytes([1]) # echo +RCP = bytes([2]) # prepare to reconnect +SGA = bytes([3]) # suppress go ahead +NAMS = bytes([4]) # approximate message size +STATUS = bytes([5]) # give status +TM = bytes([6]) # timing mark +RCTE = bytes([7]) # remote controlled transmission and echo +NAOL = bytes([8]) # negotiate about output line width +NAOP = bytes([9]) # negotiate about output page size +NAOCRD = bytes([10]) # negotiate about CR disposition +NAOHTS = bytes([11]) # negotiate about horizontal tabstops +NAOHTD = bytes([12]) # negotiate about horizontal tab disposition +NAOFFD = bytes([13]) # negotiate about formfeed disposition +NAOVTS = bytes([14]) # negotiate about vertical tab stops +NAOVTD = bytes([15]) # negotiate about vertical tab disposition +NAOLFD = bytes([16]) # negotiate about output LF disposition +XASCII = bytes([17]) # extended ascii character set +LOGOUT = bytes([18]) # force logout +BM = bytes([19]) # byte macro +DET = bytes([20]) # data entry terminal +SUPDUP = bytes([21]) # supdup protocol +SUPDUPOUTPUT = bytes([22]) # supdup output +SNDLOC = bytes([23]) # send location +TTYPE = bytes([24]) # terminal type +EOR = bytes([25]) # end or record +TUID = bytes([26]) # TACACS user identification +OUTMRK = bytes([27]) # output marking +TTYLOC = bytes([28]) # terminal location number +VT3270REGIME = bytes([29]) # 3270 regime +X3PAD = bytes([30]) # X.3 PAD +NAWS = bytes([31]) # window size +TSPEED = bytes([32]) # terminal speed +LFLOW = bytes([33]) # remote flow control +LINEMODE = bytes([34]) # Linemode option +XDISPLOC = bytes([35]) # X Display Location +OLD_ENVIRON = bytes([36]) # Old - Environment variables +AUTHENTICATION = bytes([37]) # Authenticate +ENCRYPT = bytes([38]) # Encryption option +NEW_ENVIRON = bytes([39]) # New - Environment variables +# the following ones come from +# http://www.iana.org/assignments/telnet-options +# Unfortunately, that document does not assign identifiers +# to all of them, so we are making them up +TN3270E = bytes([40]) # TN3270E +XAUTH = bytes([41]) # XAUTH +CHARSET = bytes([42]) # CHARSET +RSP = bytes([43]) # Telnet Remote Serial Port +COM_PORT_OPTION = bytes([44]) # Com Port Control Option +SUPPRESS_LOCAL_ECHO = bytes([45]) # Telnet Suppress Local Echo +TLS = bytes([46]) # Telnet Start TLS +KERMIT = bytes([47]) # KERMIT +SEND_URL = bytes([48]) # SEND-URL +FORWARD_X = bytes([49]) # FORWARD_X +PRAGMA_LOGON = bytes([138]) # TELOPT PRAGMA LOGON +SSPI_LOGON = bytes([139]) # TELOPT SSPI LOGON +PRAGMA_HEARTBEAT = bytes([140]) # TELOPT PRAGMA HEARTBEAT +EXOPL = bytes([255]) # Extended-Options-List +NOOPT = bytes([0]) + + +# poll/select have the advantage of not requiring any extra file descriptor, +# contrarily to epoll/kqueue (also, they require a single syscall). +if hasattr(selectors, "PollSelector"): + _TelnetSelector = selectors.PollSelector +else: + _TelnetSelector = selectors.SelectSelector + + +class Telnet: + """Telnet interface class. + + An instance of this class represents a connection to a telnet + server. The instance is initially not connected; the open() + method must be used to establish a connection. Alternatively, the + host name and optional port number can be passed to the + constructor, too. + + Don't try to reopen an already connected instance. + + This class has many read_*() methods. Note that some of them + raise EOFError when the end of the connection is read, because + they can return an empty string for other reasons. See the + individual doc strings. + + read_until(expected, [timeout]) + Read until the expected string has been seen, or a timeout is + hit (default is no timeout); may block. + + read_all() + Read all data until EOF; may block. + + read_some() + Read at least one byte or EOF; may block. + + read_very_eager() + Read all data available already queued or on the socket, + without blocking. + + read_eager() + Read either data already queued or some data available on the + socket, without blocking. + + read_lazy() + Read all data in the raw queue (processing it first), without + doing any socket I/O. + + read_very_lazy() + Reads all data in the cooked queue, without doing any socket + I/O. + + read_sb_data() + Reads available data between SB ... SE sequence. Don't block. + + set_option_negotiation_callback(callback) + Each time a telnet option is read on the input flow, this callback + (if set) is called with the following parameters : + callback(telnet socket, command, option) + option will be chr(0) when there is no option. + No other action is done afterwards by telnetlib. + + """ + + def __init__(self, host=None, port=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + """Constructor. + + When called without arguments, create an unconnected instance. + With a hostname argument, it connects the instance; port number + and timeout are optional. + """ + self.debuglevel = DEBUGLEVEL + self.host = host + self.port = port + self.timeout = timeout + self.sock = None + self.rawq = b"" + self.irawq = 0 + self.cookedq = b"" + self.eof = 0 + self.iacseq = b"" # Buffer for IAC sequence. + self.sb = 0 # flag for SB and SE sequence. + self.sbdataq = b"" + self.option_callback = None + if host is not None: + self.open(host, port, timeout) + + def open(self, host, port=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + """Connect to a host. + + The optional second argument is the port number, which + defaults to the standard telnet port (23). + + Don't try to reopen an already connected instance. + """ + self.eof = 0 + if not port: + port = TELNET_PORT + self.host = host + self.port = port + self.timeout = timeout + sys.audit("telnetlib.Telnet.open", self, host, port) + self.sock = socket.create_connection((host, port), timeout) + + def __del__(self): + """Destructor -- close the connection.""" + self.close() + + def msg(self, msg, *args): + """Print a debug message, when the debug level is > 0. + + If extra arguments are present, they are substituted in the + message using the standard string formatting operator. + + """ + if self.debuglevel > 0: + print("Telnet(%s,%s):" % (self.host, self.port), end=" ") + if args: + print(msg % args) + else: + print(msg) + + def set_debuglevel(self, debuglevel): + """Set the debug level. + + The higher it is, the more debug output you get (on sys.stdout). + + """ + self.debuglevel = debuglevel + + def close(self): + """Close the connection.""" + sock = self.sock + self.sock = None + self.eof = True + self.iacseq = b"" + self.sb = 0 + if sock: + sock.close() + + def get_socket(self): + """Return the socket object used internally.""" + return self.sock + + def fileno(self): + """Return the fileno() of the socket object used internally.""" + return self.sock.fileno() + + def write(self, buffer): + """Write a string to the socket, doubling any IAC characters. + + Can block if the connection is blocked. May raise + OSError if the connection is closed. + + """ + if IAC in buffer: + buffer = buffer.replace(IAC, IAC + IAC) + sys.audit("telnetlib.Telnet.write", self, buffer) + self.msg("send %r", buffer) + self.sock.sendall(buffer) + + def read_until(self, match, timeout=None): + """Read until a given string is encountered or until timeout. + + When no match is found, return whatever is available instead, + possibly the empty string. Raise EOFError if the connection + is closed and no cooked data is available. + + """ + n = len(match) + self.process_rawq() + i = self.cookedq.find(match) + if i >= 0: + i = i + n + buf = self.cookedq[:i] + self.cookedq = self.cookedq[i:] + return buf + if timeout is not None: + deadline = _time() + timeout + with _TelnetSelector() as selector: + selector.register(self, selectors.EVENT_READ) + while not self.eof: + if selector.select(timeout): + i = max(0, len(self.cookedq) - n) + self.fill_rawq() + self.process_rawq() + i = self.cookedq.find(match, i) + if i >= 0: + i = i + n + buf = self.cookedq[:i] + self.cookedq = self.cookedq[i:] + return buf + if timeout is not None: + timeout = deadline - _time() + if timeout < 0: + break + return self.read_very_lazy() + + def read_all(self): + """Read all data until EOF; block until connection closed.""" + self.process_rawq() + while not self.eof: + self.fill_rawq() + self.process_rawq() + buf = self.cookedq + self.cookedq = b"" + return buf + + def read_some(self): + """Read at least one byte of cooked data unless EOF is hit. + + Return b'' if EOF is hit. Block if no data is immediately + available. + + """ + self.process_rawq() + while not self.cookedq and not self.eof: + self.fill_rawq() + self.process_rawq() + buf = self.cookedq + self.cookedq = b"" + return buf + + def read_very_eager(self): + """Read everything that's possible without blocking in I/O (eager). + + Raise EOFError if connection closed and no cooked data + available. Return b'' if no cooked data available otherwise. + Don't block unless in the midst of an IAC sequence. + + """ + self.process_rawq() + while not self.eof and self.sock_avail(): + self.fill_rawq() + self.process_rawq() + return self.read_very_lazy() + + def read_eager(self): + """Read readily available data. + + Raise EOFError if connection closed and no cooked data + available. Return b'' if no cooked data available otherwise. + Don't block unless in the midst of an IAC sequence. + + """ + self.process_rawq() + while not self.cookedq and not self.eof and self.sock_avail(): + self.fill_rawq() + self.process_rawq() + return self.read_very_lazy() + + def read_lazy(self): + """Process and return data that's already in the queues (lazy). + + Raise EOFError if connection closed and no data available. + Return b'' if no cooked data available otherwise. Don't block + unless in the midst of an IAC sequence. + + """ + self.process_rawq() + return self.read_very_lazy() + + def read_very_lazy(self): + """Return any data available in the cooked queue (very lazy). + + Raise EOFError if connection closed and no data available. + Return b'' if no cooked data available otherwise. Don't block. + + """ + buf = self.cookedq + self.cookedq = b"" + if not buf and self.eof and not self.rawq: + raise EOFError("telnet connection closed") + return buf + + def read_sb_data(self): + """Return any data available in the SB ... SE queue. + + Return b'' if no SB ... SE available. Should only be called + after seeing a SB or SE command. When a new SB command is + found, old unread SB data will be discarded. Don't block. + + """ + buf = self.sbdataq + self.sbdataq = b"" + return buf + + def set_option_negotiation_callback(self, callback): + """Provide a callback function called after each receipt of a telnet option.""" + self.option_callback = callback + + def process_rawq(self): + """Transfer from raw queue to cooked queue. + + Set self.eof when connection is closed. Don't block unless in + the midst of an IAC sequence. + + """ + buf = [b"", b""] + try: + while self.rawq: + c = self.rawq_getchar() + if not self.iacseq: + if c == theNULL: + continue + if c == b"\021": + continue + if c != IAC: + buf[self.sb] = buf[self.sb] + c + continue + else: + self.iacseq += c + elif len(self.iacseq) == 1: + # 'IAC: IAC CMD [OPTION only for WILL/WONT/DO/DONT]' + if c in (DO, DONT, WILL, WONT): + self.iacseq += c + continue + + self.iacseq = b"" + if c == IAC: + buf[self.sb] = buf[self.sb] + c + else: + if c == SB: # SB ... SE start. + self.sb = 1 + self.sbdataq = b"" + elif c == SE: + self.sb = 0 + self.sbdataq = self.sbdataq + buf[1] + buf[1] = b"" + if self.option_callback: + # Callback is supposed to look into + # the sbdataq + self.option_callback(self.sock, c, NOOPT) + else: + # We can't offer automatic processing of + # suboptions. Alas, we should not get any + # unless we did a WILL/DO before. + self.msg("IAC %d not recognized" % ord(c)) + elif len(self.iacseq) == 2: + cmd = self.iacseq[1:2] + self.iacseq = b"" + opt = c + if cmd in (DO, DONT): + self.msg("IAC %s %d", cmd == DO and "DO" or "DONT", ord(opt)) + if self.option_callback: + self.option_callback(self.sock, cmd, opt) + else: + self.sock.sendall(IAC + WONT + opt) + elif cmd in (WILL, WONT): + self.msg( + "IAC %s %d", cmd == WILL and "WILL" or "WONT", ord(opt) + ) + if self.option_callback: + self.option_callback(self.sock, cmd, opt) + else: + self.sock.sendall(IAC + DONT + opt) + except EOFError: # raised by self.rawq_getchar() + self.iacseq = b"" # Reset on EOF + self.sb = 0 + self.cookedq = self.cookedq + buf[0] + self.sbdataq = self.sbdataq + buf[1] + + def rawq_getchar(self): + """Get next char from raw queue. + + Block if no data is immediately available. Raise EOFError + when connection is closed. + + """ + if not self.rawq: + self.fill_rawq() + if self.eof: + raise EOFError + c = self.rawq[self.irawq : self.irawq + 1] + self.irawq = self.irawq + 1 + if self.irawq >= len(self.rawq): + self.rawq = b"" + self.irawq = 0 + return c + + def fill_rawq(self): + """Fill raw queue from exactly one recv() system call. + + Block if no data is immediately available. Set self.eof when + connection is closed. + + """ + if self.irawq >= len(self.rawq): + self.rawq = b"" + self.irawq = 0 + # The buffer size should be fairly small so as to avoid quadratic + # behavior in process_rawq() above + buf = self.sock.recv(50) + self.msg("recv %r", buf) + self.eof = not buf + self.rawq = self.rawq + buf + + def sock_avail(self): + """Test whether data is available on the socket.""" + with _TelnetSelector() as selector: + selector.register(self, selectors.EVENT_READ) + return bool(selector.select(0)) + + def interact(self): + """Interaction function, emulates a very dumb telnet client.""" + if sys.platform == "win32": + self.mt_interact() + return + with _TelnetSelector() as selector: + selector.register(self, selectors.EVENT_READ) + selector.register(sys.stdin, selectors.EVENT_READ) + + while True: + for key, events in selector.select(): + if key.fileobj is self: + try: + text = self.read_eager() + except EOFError: + print("*** Connection closed by remote host ***") + return + if text: + sys.stdout.write(text.decode("ascii")) + sys.stdout.flush() + elif key.fileobj is sys.stdin: + line = sys.stdin.readline().encode("ascii") + if not line: + return + self.write(line) + + def mt_interact(self): + """Multithreaded version of interact().""" + import _thread + + _thread.start_new_thread(self.listener, ()) + while 1: + line = sys.stdin.readline() + if not line: + break + self.write(line.encode("ascii")) + + def listener(self): + """Helper for mt_interact() -- this executes in the other thread.""" + while 1: + try: + data = self.read_eager() + except EOFError: + print("*** Connection closed by remote host ***") + return + if data: + sys.stdout.write(data.decode("ascii")) + else: + sys.stdout.flush() + + def expect(self, list, timeout=None): + """Read until one from a list of a regular expressions matches. + + The first argument is a list of regular expressions, either + compiled (re.Pattern instances) or uncompiled (strings). + The optional second argument is a timeout, in seconds; default + is no timeout. + + Return a tuple of three items: the index in the list of the + first regular expression that matches; the re.Match object + returned; and the text read up till and including the match. + + If EOF is read and no text was read, raise EOFError. + Otherwise, when nothing matches, return (-1, None, text) where + text is the text received so far (may be the empty string if a + timeout happened). + + If a regular expression ends with a greedy match (e.g. '.*') + or if more than one expression can match the same input, the + results are undeterministic, and may depend on the I/O timing. + + """ + re = None + list = list[:] + indices = range(len(list)) + for i in indices: + if not hasattr(list[i], "search"): + if not re: + import re + list[i] = re.compile(list[i]) + if timeout is not None: + deadline = _time() + timeout + with _TelnetSelector() as selector: + selector.register(self, selectors.EVENT_READ) + while not self.eof: + self.process_rawq() + for i in indices: + m = list[i].search(self.cookedq) + if m: + e = m.end() + text = self.cookedq[:e] + self.cookedq = self.cookedq[e:] + return (i, m, text) + if timeout is not None: + ready = selector.select(timeout) + timeout = deadline - _time() + if not ready: + if timeout < 0: + break + else: + continue + self.fill_rawq() + text = self.read_very_lazy() + if not text and self.eof: + raise EOFError + return (-1, None, text) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + +def test(): + """Test program for telnetlib. + + Usage: python telnetlib.py [-d] ... [host [port]] + + Default host is localhost; default port is 23. + + """ + debuglevel = 0 + while sys.argv[1:] and sys.argv[1] == "-d": + debuglevel = debuglevel + 1 + del sys.argv[1] + host = "localhost" + if sys.argv[1:]: + host = sys.argv[1] + port = 0 + if sys.argv[2:]: + portstr = sys.argv[2] + try: + port = int(portstr) + except ValueError: + port = socket.getservbyname(portstr, "tcp") + with Telnet() as tn: + tn.set_debuglevel(debuglevel) + tn.open(host, port, timeout=0.5) + tn.interact() + + +if __name__ == "__main__": + test() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/connection_base.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/connection_base.py new file mode 100644 index 0000000..48293ca --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/connection_base.py @@ -0,0 +1,89 @@ +# (c) 2012-2014, Michael DeHaan +# (c) 2015 Toshio Kuratomi +# (c) 2017, Peter Sprygada +# (c) 2017 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 + + +from ansible import constants as C +from ansible.utils.display import Display +from ansible_collections.ansible.utils.plugins.plugin_utils.connection_base import ( + PersistentConnectionBase, +) + + +display = Display() + + +__all__ = ["NetworkConnectionBase"] + +BUFSIZE = 65536 + + +class NetworkConnectionBase(PersistentConnectionBase): + """ + A base class for network-style connections using a sub-plugin. + """ + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(NetworkConnectionBase, self).__init__(play_context, new_stdin, *args, **kwargs) + self._network_os = self._play_context.network_os + self._sub_plugin = {} + self._cached_variables = (None, None, None) + + def __getattr__(self, name): + try: + return self.__dict__[name] + except KeyError: + if not name.startswith("_"): + plugin = self._sub_plugin.get("obj") + if plugin: + method = getattr(plugin, name, None) + if method is not None: + return method + raise AttributeError( + "'%s' object has no attribute '%s'" % (self.__class__.__name__, name) + ) + + def get_options(self, hostvars=None): + options = super(NetworkConnectionBase, self).get_options(hostvars=hostvars) + + if self._sub_plugin.get("obj") and self._sub_plugin.get("type") != "external": + try: + options.update(self._sub_plugin["obj"].get_options(hostvars=hostvars)) + except AttributeError: + pass + + return options + + def set_options(self, task_keys=None, var_options=None, direct=None): + super(NetworkConnectionBase, self).set_options( + task_keys=task_keys, var_options=var_options, direct=direct + ) + if self.get_option("persistent_log_messages"): + warning = ( + "Persistent connection logging is enabled for %s. This will log ALL interactions" + % self._play_context.remote_addr + ) + logpath = getattr(C, "DEFAULT_LOG_PATH") + if logpath is not None: + warning += " to %s" % logpath + self.queue_message( + "warning", + "%s and WILL NOT redact sensitive configuration like passwords. USE WITH CAUTION!" + % warning, + ) + + if self._sub_plugin.get("obj") and self._sub_plugin.get("type") != "external": + try: + self._sub_plugin["obj"].set_options( + task_keys=task_keys, var_options=var_options, direct=direct + ) + except AttributeError: + pass diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/hash_salt.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/hash_salt.py new file mode 100644 index 0000000..e49d401 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/hash_salt.py @@ -0,0 +1,28 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The hash_salt plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.errors import AnsibleFilterError + + +def _raise_error(msg): + raise AnsibleFilterError(msg) + + +def hash_salt(password): + split_password = password.split("$") + if len(split_password) != 4: + _raise_error("Could not parse salt out password correctly from {0}".format(password)) + else: + return split_password[2] diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/httpapi_base.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/httpapi_base.py new file mode 100644 index 0000000..3576435 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/httpapi_base.py @@ -0,0 +1,94 @@ +# (c) 2018 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 + +from abc import abstractmethod + +# Needed to satisfy PluginLoader's required_base_class +from ansible.plugins.httpapi import HttpApiBase as HttpApiBaseBase + + +class HttpApiBase(HttpApiBaseBase): + def __init__(self, connection): + super(HttpApiBase, self).__init__(connection) + + self.connection = connection + self._become = False + self._become_pass = "" + + def set_become(self, become_context): + self._become = become_context.become + self._become_pass = getattr(become_context, "become_pass") or "" + + def login(self, username, password): + """Call a defined login endpoint to receive an authentication token. + + This should only be implemented if the API has a single endpoint which + can turn HTTP basic auth into a token which can be reused for the rest + of the calls for the session. + """ + pass + + def logout(self): + """Call to implement session logout. + + Method to clear session gracefully e.g. tokens granted in login + need to be revoked. + """ + pass + + def update_auth(self, response, response_text): + """Return per-request auth token. + + The response should be a dictionary that can be plugged into the + headers of a request. The default implementation uses cookie data. + If no authentication data is found, return None + """ + cookie = response.info().get("Set-Cookie") + if cookie: + return {"Cookie": cookie} + + return None + + def handle_httperror(self, exc): + """Overridable method for dealing with HTTP codes. + + This method will attempt to handle known cases of HTTP status codes. + If your API uses status codes to convey information in a regular way, + you can override this method to handle it appropriately. + + :returns: + * True if the code has been handled in a way that the request + may be resent without changes. + * False if the error cannot be handled or recovered from by the + plugin. This will result in the HTTPError being raised as an + exception for the caller to deal with as appropriate (most likely + by failing). + * Any other value returned is taken as a valid response from the + server without making another request. In many cases, this can just + be the original exception. + """ + if exc.code == 401: + if self.connection._auth: + # Stored auth appears to be invalid, clear and retry + self.connection._auth = None + self.login( + self.connection.get_option("remote_user"), + self.connection.get_option("password"), + ) + return True + else: + # Unauthorized and there's no token. Return an error + return False + + return exc + + @abstractmethod + def send_request(self, data, **message_kwargs): + """Prepares and sends request(s) to device.""" + pass diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/netconf_base.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/netconf_base.py new file mode 100644 index 0000000..81645fd --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/netconf_base.py @@ -0,0 +1,404 @@ +# +# (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 + +from abc import abstractmethod +from functools import wraps + +from ansible.errors import AnsibleError +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native + +# Needed to satisfy PluginLoader's required_base_class +from ansible.plugins.netconf import NetconfBase as NetconfBaseBase + + +try: + from ncclient.operations import RPCError + from ncclient.xml_ import NCElement, to_ele, to_xml + + HAS_NCCLIENT = True + NCCLIENT_IMP_ERR = None +# paramiko and gssapi are incompatible and raise AttributeError not ImportError +# When running in FIPS mode, cryptography raises InternalError +# https://bugzilla.redhat.com/show_bug.cgi?id=1778939 +except Exception as err: + HAS_NCCLIENT = False + NCCLIENT_IMP_ERR = err + +try: + from lxml.etree import fromstring +except ImportError: + from xml.etree.ElementTree import fromstring + + +def ensure_ncclient(func): + @wraps(func) + def wrapped(self, *args, **kwargs): + if not HAS_NCCLIENT: + raise AnsibleError( + "%s: %s" + % ( + missing_required_lib("ncclient"), + to_native(NCCLIENT_IMP_ERR), + ) + ) + return func(self, *args, **kwargs) + + return wrapped + + +class NetconfBase(NetconfBaseBase): + """ + A base class for implementing Netconf connections + + .. note:: Unlike most of Ansible, nearly all strings in + :class:`TerminalBase` plugins are byte strings. This is because of + how close to the underlying platform these plugins operate. Remember + to mark literal strings as byte string (``b"string"``) and to use + :func:`~ansible.module_utils.common.text.converters.to_bytes` and + :func:`~ansible.module_utils.common.text.converters.to_text` to avoid + unexpected problems. + + List of supported rpc's: + :get: Retrieves running configuration and device state information + :get_config: Retrieves the specified configuration from the device + :edit_config: Loads the specified commands into the remote device + :commit: Load configuration from candidate to running + :discard_changes: Discard changes to candidate datastore + :validate: Validate the contents of the specified configuration. + :lock: Allows the client to lock the configuration system of a device. + :unlock: Release a configuration lock, previously obtained with the lock operation. + :copy_config: create or replace an entire configuration datastore with the contents of another complete + configuration datastore. + :get-schema: Retrieves the required schema from the device + :get_capabilities: Retrieves device information and supported rpc methods + + For JUNOS: + :execute_rpc: RPC to be execute on remote device + :load_configuration: Loads given configuration on device + + Note: rpc support depends on the capabilites of remote device. + + :returns: Returns output received from remote device as byte string + Note: the 'result' or 'error' from response should to be converted to object + of ElementTree using 'fromstring' to parse output as xml doc + + 'get_capabilities()' returns 'result' as a json string. + + Usage: + from ansible.module_utils.connection import Connection + + conn = Connection() + data = conn.execute_rpc(rpc) + reply = fromstring(reply) + + data = conn.get_capabilities() + json.loads(data) + + conn.load_configuration(config=[''set system ntp server 1.1.1.1''], action='set', format='text') + """ + + __rpc__ = [ + "rpc", + "get_config", + "get", + "edit_config", + "validate", + "copy_config", + "dispatch", + "lock", + "unlock", + "discard_changes", + "commit", + "get_schema", + "delete_config", + "get_device_operations", + ] + + def __init__(self, connection): + super(NetconfBase, self).__init__(connection) + self._connection = connection + + @property + def m(self): + return self._connection.manager + + def rpc(self, name): + """ + RPC to be execute on remote device + :param name: Name of rpc in string format + :return: Received rpc response from remote host + """ + try: + obj = to_ele(name) + resp = self.m.rpc(obj) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + except RPCError as exc: + msg = exc.xml + raise Exception(to_xml(msg)) + + def get_config(self, source=None, filter=None): + """ + Retrieve all or part of a specified configuration + (by default entire configuration is retrieved). + :param source: Name of the configuration datastore being queried, defaults to running datastore + :param filter: This argument specifies the portion of the configuration data to retrieve + :return: Returns xml string containing the RPC response received from remote host + """ + if isinstance(filter, list): + filter = tuple(filter) + + if not source: + source = "running" + resp = self.m.get_config(source=source, filter=filter) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def get(self, filter=None, with_defaults=None): + """ + Retrieve device configuration and state information. + :param filter: This argument specifies the portion of the state data to retrieve + (by default entire state data is retrieved) + :param with_defaults: defines an explicit method of retrieving default values + from the configuration + :return: Returns xml string containing the RPC response received from remote host + """ + if isinstance(filter, list): + filter = tuple(filter) + resp = self.m.get(filter=filter, with_defaults=with_defaults) + response = resp.data_xml if hasattr(resp, "data_xml") else resp.xml + return response + + def edit_config( + self, + config=None, + format="xml", + target="candidate", + default_operation=None, + test_option=None, + error_option=None, + ): + """ + Loads all or part of the specified *config* to the *target* configuration datastore. + :param config: Is the configuration, which must be rooted in the `config` element. + It can be specified either as a string or an :class:`~xml.etree.ElementTree.Element`. + :param format: The format of configuration eg. xml, text + :param target: Is the name of the configuration datastore being edited + :param default_operation: If specified must be one of { `"merge"`, `"replace"`, or `"none"` } + :param test_option: If specified must be one of { `"test_then_set"`, `"set"` } + :param error_option: If specified must be one of { `"stop-on-error"`, `"continue-on-error"`, `"rollback-on-error"` } + The `"rollback-on-error"` *error_option* depends on the `:rollback-on-error` capability. + :return: Returns xml string containing the RPC response received from remote host + """ + if config is None: + raise ValueError("config value must be provided") + resp = self.m.edit_config( + config, + format=format, + target=target, + default_operation=default_operation, + test_option=test_option, + error_option=error_option, + ) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def validate(self, source="candidate"): + """ + Validate the contents of the specified configuration. + :param source: Is the name of the configuration datastore being validated or `config` element + containing the configuration subtree to be validated + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.validate(source=source) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def copy_config(self, source, target): + """ + Create or replace an entire configuration datastore with the contents of another complete configuration datastore. + :param source: Is the name of the configuration datastore to use as the source of the copy operation or `config` + element containing the configuration subtree to copy + :param target: Is the name of the configuration datastore to use as the destination of the copy operation + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.copy_config(source, target) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def dispatch(self, rpc_command=None, source=None, filter=None): + """ + Execute rpc on the remote device eg. dispatch('clear-arp-table') + :param rpc_command: specifies rpc command to be dispatched either in plain text or in xml element format (depending on command) + :param source: name of the configuration datastore being queried + :param filter: specifies the portion of the configuration to retrieve (by default entire configuration is retrieved) + :return: Returns xml string containing the RPC response received from remote host + """ + if rpc_command is None: + raise ValueError("rpc_command value must be provided") + + resp = self.m.dispatch(fromstring(rpc_command), source=source, filter=filter) + + if isinstance(resp, NCElement): + # In case xml reply is transformed or namespace is removed in + # ncclient device specific handler return modified xml response + result = resp.data_xml + elif hasattr(resp, "data_ele") and resp.data_ele: + # if data node is present in xml response return the xml string + # with data node as root + result = resp.data_xml + else: + # return raw xml string received from host with rpc-reply as the root node + result = resp.xml + + return result + + def lock(self, target="candidate"): + """ + Allows the client to lock the configuration system of a device. + :param target: is the name of the configuration datastore to lock, + defaults to candidate datastore + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.lock(target=target) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def unlock(self, target="candidate"): + """ + Release a configuration lock, previously obtained with the lock operation. + :param target: is the name of the configuration datastore to unlock, + defaults to candidate datastore + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.unlock(target=target) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def discard_changes(self): + """ + Revert the candidate configuration to the currently running configuration. + Any uncommitted changes are discarded. + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.discard_changes() + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def commit(self, confirmed=False, timeout=None, persist=None): + """ + Commit the candidate configuration as the device's new current configuration. + Depends on the `:candidate` capability. + A confirmed commit (i.e. if *confirmed* is `True`) is reverted if there is no + followup commit within the *timeout* interval. If no timeout is specified the + confirm timeout defaults to 600 seconds (10 minutes). + A confirming commit may have the *confirmed* parameter but this is not required. + Depends on the `:confirmed-commit` capability. + :param confirmed: whether this is a confirmed commit + :param timeout: specifies the confirm timeout in seconds + :param persist: make the confirmed commit survive a session termination, + and set a token on the ongoing confirmed commit + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.commit(confirmed=confirmed, timeout=timeout, persist=persist) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def get_schema(self, identifier=None, version=None, format=None): + """ + Retrieve a named schema, with optional revision and type. + :param identifier: name of the schema to be retrieved + :param version: version of schema to get + :param format: format of the schema to be retrieved, yang is the default + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.get_schema(identifier, version=version, format=format) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def delete_config(self, target): + """ + delete a configuration datastore + :param target: specifies the name or URL of configuration datastore to delete + :return: Returns xml string containing the RPC response received from remote host + """ + resp = self.m.delete_config(target) + return resp.data_xml if hasattr(resp, "data_xml") else resp.xml + + def locked(self, target): + return self.m.locked(target) + + @abstractmethod + def get_capabilities(self): + """ + Retrieves device information and supported + rpc methods by device platform and return result + as a string + :return: Netconf session capability + """ + pass + + @staticmethod + def guess_network_os(obj): + """ + Identifies the operating system of network device. + :param obj: ncclient manager connection instance + :return: The name of network operating system. + """ + pass + + def get_base_rpc(self): + """ + Returns list of base rpc method supported by remote device + :return: List of RPC supported + """ + return self.__rpc__ + + def put_file(self, source, destination): + """ + Copies file to remote host + :param source: Source location of file + :param destination: Destination file path + :return: Returns xml string containing the RPC response received from remote host + """ + pass + + def fetch_file(self, source, destination): + """ + Fetch file from remote host + :param source: Source location of file + :param destination: Source location of file + :return: Returns xml string containing the RPC response received from remote host + """ + pass + + def get_device_operations(self, server_capabilities): + """ + Retrieve remote host capability from Netconf server hello message. + :param server_capabilities: Server capabilities received during Netconf session initialization + :return: Remote host capabilities in dictionary format + """ + operations = {} + capabilities = "\n".join(server_capabilities) + operations["supports_commit"] = ":candidate" in capabilities + operations["supports_defaults"] = ":with-defaults" in capabilities + operations["supports_confirm_commit"] = ":confirmed-commit" in capabilities + operations["supports_startup"] = ":startup" in capabilities + operations["supports_xpath"] = ":xpath" in capabilities + operations["supports_writable_running"] = ":writable-running" in capabilities + operations["supports_validate"] = ":validate" in capabilities + + operations["lock_datastore"] = [] + if operations["supports_writable_running"]: + operations["lock_datastore"].append("running") + + if operations["supports_commit"]: + operations["lock_datastore"].append("candidate") + + if operations["supports_startup"]: + operations["lock_datastore"].append("startup") + + operations["supports_lock"] = bool(operations["lock_datastore"]) + + return operations + + +# TODO Restore .xml, when ncclient supports it for all platforms diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_cli.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_cli.py new file mode 100644 index 0000000..8825d08 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_cli.py @@ -0,0 +1,224 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The parse_xml plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import re + +from collections.abc import Mapping + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + JinjaTemplate, +) + + +try: + import yaml + + HAS_YAML = True +except ImportError: + HAS_YAML = False + +string_types = (str,) + + +def _raise_error(msg): + raise AnsibleFilterError(msg) + + +def re_matchall(regex, value): + objects = list() + for match in re.findall(regex.pattern, value, re.M): + obj = {} + if regex.groupindex: + for name, index in regex.groupindex.items(): + if len(regex.groupindex) == 1: + obj[name] = match + else: + obj[name] = match[index - 1] + objects.append(obj) + return objects + + +def re_search(regex, value): + obj = {} + match = regex.search(value, re.M) + if match: + items = list(match.groups()) + if regex.groupindex: + for name, index in regex.groupindex.items(): + obj[name] = items[index - 1] + return obj + + +def re_finditer(regex, value): + iter_obj = re.finditer(regex, value) + values = None + for each in iter_obj: + if not values: + values = each.groupdict() + else: + # for backward compatibility + values.update(each.groupdict()) + # for backward compatibility + values["match"] = list(each.groups()) + groups = each.groupdict() + for group in groups: + if not values.get("match_all"): + values["match_all"] = dict() + if not values["match_all"].get(group): + values["match_all"][group] = list() + values["match_all"][group].append(groups[group]) + return values + + +def parse_cli(output, tmpl): + if not isinstance(output, string_types): + _raise_error( + "parse_cli input should be a string, but was given a input of %s" % (type(output)) + ) + + if not os.path.exists(tmpl): + _raise_error("unable to locate parse_cli template: %s" % tmpl) + + try: + template = JinjaTemplate() + except ImportError as exc: + _raise_error(to_native(exc)) + + with open(tmpl) as tmpl_fh: + tmpl_content = tmpl_fh.read() + + spec = yaml.safe_load(tmpl_content) + obj = {} + + for name, attrs in spec["keys"].items(): + value = attrs["value"] + + try: + variables = spec.get("vars", {}) + value = template(value, variables) + except Exception: + pass + + if "start_block" in attrs and "end_block" in attrs: + start_block = re.compile(attrs["start_block"]) + end_block = re.compile(attrs["end_block"]) + + blocks = list() + lines = None + block_started = False + + for line in output.split("\n"): + match_start = start_block.match(line) + match_end = end_block.match(line) + + if match_start: + lines = list() + lines.append(line) + block_started = True + + elif match_end: + if lines: + lines.append(line) + blocks.append("\n".join(lines)) + lines = None + block_started = False + + elif block_started: + if lines: + lines.append(line) + + regex_items = [re.compile(r) for r in attrs["items"]] + objects = list() + + for block in blocks: + if isinstance(value, Mapping) and "key" not in value: + items = list() + for regex in regex_items: + items.append(re_finditer(regex, block)) + + obj = {} + for k, v in value.items(): + try: + obj[k] = template(v, {"item": items}, fail_on_undefined=False) + except Exception: + obj[k] = None + objects.append(obj) + + elif isinstance(value, Mapping): + items = list() + for regex in regex_items: + items.append(re_finditer(regex, block)) + + key = template(value["key"], {"item": items}) + values = dict( + [(k, template(v, {"item": items})) for k, v in value["values"].items()] + ) + objects.append({key: values}) + + return objects + + elif "items" in attrs: + regexp = re.compile(attrs["items"]) + when = attrs.get("when") + conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" % when + + if isinstance(value, Mapping) and "key" not in value: + values = list() + + for item in re_matchall(regexp, output): + entry = {} + + for item_key, item_value in value.items(): + entry[item_key] = template(item_value, {"item": item}) + + if when: + if template(conditional, {"item": entry}): + values.append(entry) + else: + values.append(entry) + + obj[name] = values + + elif isinstance(value, Mapping): + values = dict() + + for item in re_matchall(regexp, output): + entry = {} + + for item_key, item_value in value["values"].items(): + entry[item_key] = template(item_value, {"item": item}) + + key = template(value["key"], {"item": item}) + + if when: + if template(conditional, {"item": {"key": key, "value": entry}}): + values[key] = entry + else: + values[key] = entry + + obj[name] = values + + else: + item = re_search(regexp, output) + obj[name] = template(value, {"item": item}) + + else: + obj[name] = value + + return obj diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_cli_textfsm.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_cli_textfsm.py new file mode 100644 index 0000000..a1c4f28 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_cli_textfsm.py @@ -0,0 +1,61 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The parse_cli_textfsm plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.common.text.converters import to_native + + +try: + import textfsm + + HAS_TEXTFSM = True +except ImportError: + HAS_TEXTFSM = False + +string_types = (str,) + + +def _raise_error(msg): + raise AnsibleFilterError(msg) + + +def parse_cli_textfsm(value, template): + if not HAS_TEXTFSM: + _raise_error("parse_cli_textfsm filter requires TextFSM library to be installed") + + if not isinstance(value, string_types): + _raise_error( + "parse_cli_textfsm input should be a string, but was given a input of %s" + % (type(value)) + ) + + if not os.path.exists(template): + _raise_error("unable to locate parse_cli_textfsm template: %s" % template) + + try: + template = open(template) + except IOError as exc: + _raise_error(to_native(exc)) + + re_table = textfsm.TextFSM(template) + fsm_results = re_table.ParseText(value) + + results = list() + for item in fsm_results: + results.append(dict(zip(re_table.header, item))) + + return results diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_xml.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_xml.py new file mode 100644 index 0000000..0044e74 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/parse_xml.py @@ -0,0 +1,154 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The parse_xml plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import os +import traceback + +from collections.abc import Mapping +from xml.etree.ElementTree import fromstring + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.common.text.converters import to_native +from ansible.utils.display import Display + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + JinjaTemplate, +) + + +string_types = (str,) + +try: + import yaml + + HAS_YAML = True +except ImportError: + HAS_YAML = False + +display = Display() + + +def _raise_error(msg): + raise AnsibleFilterError(msg) + + +def _extract_param(template, root, attrs, value): + key = None + when = attrs.get("when") + conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}" % when + param_to_xpath_map = attrs["items"] + + if isinstance(value, Mapping): + key = value.get("key", None) + if key: + value = value["values"] + + entries = dict() if key else list() + + for element in root.findall(attrs["top"]): + entry = dict() + item_dict = dict() + for param, param_xpath in param_to_xpath_map.items(): + fields = None + try: + fields = element.findall(param_xpath) + except Exception: + display.warning( + "Failed to evaluate value of '%s' with XPath '%s'.\nUnexpected error: %s." + % (param, param_xpath, traceback.format_exc()) + ) + + tags = param_xpath.split("/") + + # check if xpath ends with attribute. + # If yes set attribute key/value dict to param value in case attribute matches + # else if it is a normal xpath assign matched element text value. + if len(tags) and tags[-1].endswith("]"): + if fields: + if len(fields) > 1: + item_dict[param] = [field.attrib for field in fields] + else: + item_dict[param] = fields[0].attrib + else: + item_dict[param] = {} + else: + if fields: + if len(fields) > 1: + item_dict[param] = [field.text for field in fields] + else: + item_dict[param] = fields[0].text + else: + item_dict[param] = None + + if isinstance(value, Mapping): + for item_key, item_value in value.items(): + entry[item_key] = template(item_value, {"item": item_dict}) + else: + entry = template(value, {"item": item_dict}) + + if key: + expanded_key = template(key, {"item": item_dict}) + if when: + if template( + conditional, + {"item": {"key": expanded_key, "value": entry}}, + ): + entries[expanded_key] = entry + else: + entries[expanded_key] = entry + else: + if when: + if template(conditional, {"item": entry}): + entries.append(entry) + else: + entries.append(entry) + + return entries + + +def parse_xml(output, tmpl): + if not os.path.exists(tmpl): + _raise_error("unable to locate parse_xml template: %s" % tmpl) + + if not isinstance(output, string_types): + _raise_error("parse_xml works on string input, but given input of : %s" % type(output)) + + root = fromstring(output) + try: + template = JinjaTemplate() + except ImportError as exc: + raise AnsibleFilterError(to_native(exc)) + + with open(tmpl) as tmpl_fh: + tmpl_content = tmpl_fh.read() + + spec = yaml.safe_load(tmpl_content) + obj = {} + + for name, attrs in spec["keys"].items(): + value = attrs["value"] + + try: + variables = spec.get("vars", {}) + value = template(value, variables) + except Exception: + pass + + if "items" in attrs: + obj[name] = _extract_param(template, root, attrs, value) + else: + obj[name] = value + + return obj diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/pop_ace.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/pop_ace.py new file mode 100644 index 0000000..e55a153 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/pop_ace.py @@ -0,0 +1,170 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The pop_ace plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.errors import AnsibleFilterError + + +def _raise_error(msg): + """Raise an error message, prepend with filter name + :param msg: The message + :type msg: str + :raises: AnsibleError + """ + error = "Error when using plugin 'pop_ace': {msg}".format(msg=msg) + raise AnsibleFilterError(error) + + +def fail_missing(racl, fail): + if fail and racl == []: + _raise_error("no entries removed on the provided match_criteria") + + +def check_match(ace, match_criteria, match_all, name, afi): + check_arr = [] + for k, v in match_criteria.items(): + if v: + if k not in ["source", "destination", "acl_name", "afi"]: + ( + check_arr.append(True) + if ace.get(k, "NA") + == match_criteria.get( + k, + ) + else check_arr.append(False) + ) + elif k == "acl_name": + ( + check_arr.append(True) + if name + == match_criteria.get( + k, + ) + else check_arr.append(False) + ) + elif k == "afi": + ( + check_arr.append(True) + if afi + == match_criteria.get( + k, + ) + else check_arr.append(False) + ) + else: # for source and destination address + _sub = "source" if "source" in k else "destination" + _valid = [] + ( + _valid.append(True) + if ace.get(_sub, {}).get("address", "NA") + == match_criteria.get( + k, + ) + else _valid.append(False) + ) + ( + _valid.append(True) + if ace.get(_sub, {}).get("host", "NA") + == match_criteria.get( + k, + ) + else _valid.append(False) + ) + ( + _valid.append(True) + if ace.get(_sub, {}).get("any", "NA") == (match_criteria.get(k) == "any") + else _valid.append(False) + ) + check_arr.append(any(_valid)) + + if match_all: # forces all criteria to match + return all(check_arr) + else: + return any(check_arr) + + +def _pop_ace(raw_acl, filter_options, match_criteria): + acls_v4, acls_v6 = [], [] + racls_v4, racls_v6 = [], [] + + remove_first_ace_only = True if filter_options.get("remove") == "first" else False + fail_if_no_match = True if filter_options.get("failed_when") == "missing" else False + match_all = True if filter_options.get("match_all") is True else False + + final_acl = { + "acls": [ + {"acls": acls_v4, "afi": "ipv4"}, + {"acls": acls_v6, "afi": "ipv6"}, + ], + } # holds final acl data after removal of aces + rfinal_acl = { + "acls": [ + {"acls": racls_v4, "afi": "ipv4"}, + {"acls": racls_v6, "afi": "ipv6"}, + ], + } # holds removed acl information + + for acls in raw_acl: # ["acls"] + afi = acls.get("afi") # ipv4 or v6 + + for acl in acls.get("acls"): + _aces, _acl = [], {} + _races, _racl, _rstop = [], {}, True + + aces = acl.get("aces", {}) + name = acl.get("name", "") # filter by acl_name ignores whole acl entries i.e all aces + + for ace in aces: # iterate on ace entries + judge = check_match( + ace=ace, + match_criteria=match_criteria, + match_all=match_all, + name=name, + afi=afi, + ) + if judge: # check matching criteria and remove from final dict + if remove_first_ace_only and _rstop: # removes one ace entry per acl + _races.append(ace) + _rstop = False + continue + elif not remove_first_ace_only: # for remove all + _races.append(ace) + continue + + _aces.append( + ace, + ) + + if _aces: # store filtered aces + _acl["name"], _acl["aces"] = name, _aces + acls_v4.append(_acl) if afi == "ipv4" else acls_v6.append(_acl) + + if _races: # store removed aces + _racl["name"], _racl["aces"] = name, _races + racls_v4.append(_racl) if afi == "ipv4" else racls_v6.append(_racl) + + fail_missing(racls_v4 + racls_v6, fail_if_no_match) + + return final_acl, rfinal_acl + + +def pop_ace(data, filter_options, match_criteria): + if not isinstance(data, (list, dict)): + _raise_error("Input is not valid for pop_ace") + cleared_data, removed_data = _pop_ace(data, filter_options, match_criteria) + data = { + "clean_acls": cleared_data, + "removed_aces": removed_data, + } + return data diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/terminal_base.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/terminal_base.py new file mode 100644 index 0000000..104667e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/terminal_base.py @@ -0,0 +1,121 @@ +# +# (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 + +# Needed to satisfy PluginLoader's required_base_class +from ansible.plugins.terminal import TerminalBase as TerminalBaseBase + + +class TerminalBase(TerminalBaseBase): + """ + A base class for implementing cli connections + + .. note:: Unlike most of Ansible, nearly all strings in + :class:`TerminalBase` plugins are byte strings. This is because of + how close to the underlying platform these plugins operate. Remember + to mark literal strings as byte string (``b"string"``) and to use + :func:`~ansible.module_utils.common.text.converters.to_bytes` and + :func:`~ansible.module_utils.common.text.converters.to_text` to avoid + unexpected problems. + """ + + #: compiled bytes regular expressions as stdout + terminal_stdout_re = [] + + #: compiled bytes regular expressions as stderr + terminal_stderr_re = [] + + #: compiled bytes regular expressions to remove ANSI codes + ansi_re = [ + re.compile(rb"\x1b\[\?1h\x1b="), # CSI ? 1 h ESC = + re.compile(rb"\x08."), # [Backspace] . + re.compile(rb"\x1b\[m"), # ANSI reset code + ] + + #: terminal initial prompt + terminal_initial_prompt = None + + #: terminal initial answer + terminal_initial_answer = None + + #: Send newline after prompt match + terminal_inital_prompt_newline = True + + def __init__(self, connection): + super(TerminalBase, self).__init__(connection) + self._connection = connection + + def _exec_cli_command(self, cmd, check_rc=True): + """ + Executes the CLI command on the remote device and returns the output + + :arg cmd: Byte string command to be executed + """ + return self._connection.exec_command(cmd) + + def _get_prompt(self): + """ + Returns the current prompt from the device + + :returns: A byte string of the prompt + """ + return self._connection.get_prompt() + + def on_open_shell(self): + """Called after the SSH session is established + + This method is called right after the invoke_shell() is called from + the Paramiko SSHClient instance. It provides an opportunity to setup + terminal parameters such as disbling paging for instance. + """ + pass + + def on_close_shell(self): + """Called before the connection is closed + + This method gets called once the connection close has been requested + but before the connection is actually closed. It provides an + opportunity to clean up any terminal resources before the shell is + actually closed + """ + pass + + def on_become(self, passwd=None): + """Called when privilege escalation is requested + + :kwarg passwd: String containing the password + + This method is called when the privilege is requested to be elevated + in the play context by setting become to True. It is the responsibility + of the terminal plugin to actually do the privilege escalation such + as entering `enable` mode for instance + """ + pass + + def on_unbecome(self): + """Called when privilege deescalation is requested + + This method is called when the privilege changed from escalated + (become=True) to non escalated (become=False). It is the responsibility + of this method to actually perform the deauthorization procedure + """ + pass + + def on_authorize(self, passwd=None): + """Deprecated method for privilege escalation + + :kwarg passwd: String containing the password + """ + return self.on_become(passwd) + + def on_deauthorize(self): + """Deprecated method for privilege deescalation""" + return self.on_unbecome() diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/type5_pw.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/type5_pw.py new file mode 100644 index 0000000..5f26d60 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/type5_pw.py @@ -0,0 +1,64 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The type5_pw plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import string + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.common.text.converters import to_text + + +try: + # old import + HAS_PASSLIB_OR_CRYPT = True + from ansible.utils.encrypt import passlib_or_crypt, random_password +except ImportError: + # To use do_encrypt from 2.20 + from ansible.utils.encrypt import random_password + + HAS_PASSLIB_OR_CRYPT = False + +string_types = (str,) + + +def _raise_error(msg): + raise AnsibleFilterError(msg) + + +def type5_pw(password, salt=None): + if not isinstance(password, string_types): + _raise_error( + "type5_pw password input should be a string, but was given a input of %s" + % (type(password).__name__) + ) + + salt_chars = "".join((to_text(string.ascii_letters), to_text(string.digits), "./")) + if salt is not None and not isinstance(salt, string_types): + _raise_error( + "type5_pw salt input should be a string, but was given a input of %s" + % (type(salt).__name__) + ) + elif not salt: + salt = random_password(length=4, chars=salt_chars) + elif not set(salt) <= set(salt_chars): + _raise_error("type5_pw salt used inproper characters, must be one of %s" % (salt_chars)) + + if HAS_PASSLIB_OR_CRYPT: + encrypted_password = passlib_or_crypt(password, "md5_crypt", salt=salt) + else: + from ansible.utils.encrypt import do_encrypt + + encrypted_password = do_encrypt(password, "md5_crypt", salt=salt) + + return encrypted_password diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/version.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/version.py new file mode 100644 index 0000000..a33808e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/version.py @@ -0,0 +1,40 @@ +# (c) 2022 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 + +from functools import total_ordering + + +@total_ordering +class Version: + """Simple class to compare arbitrary versions""" + + def __init__(self, version_string): + self.components = version_string.split(".") + + def __eq__(self, other): + other = _coerce(other) + if not isinstance(other, Version): + return NotImplemented + + return self.components == other.components + + def __lt__(self, other): + other = _coerce(other) + if not isinstance(other, Version): + return NotImplemented + + return self.components < other.components + + +def _coerce(other): + if isinstance(other, str): + other = Version(other) + if isinstance(other, (int, float)): + other = Version(str(other)) + return other diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/vlan_expander.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/vlan_expander.py new file mode 100644 index 0000000..7767992 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/vlan_expander.py @@ -0,0 +1,25 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The vlan_expander plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +def vlan_expander(data): + expanded_list = [] + for each in data.split(","): + if "-" in each: + f, t = map(int, each.split("-")) + expanded_list.extend(range(f, t + 1)) + else: + expanded_list.append(int(each)) + return sorted(expanded_list) diff --git a/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/vlan_parser.py b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/vlan_parser.py new file mode 100644 index 0000000..1fcef54 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/plugin_utils/vlan_parser.py @@ -0,0 +1,93 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +""" +The vlan_parser plugin code +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from ansible.errors import AnsibleFilterError + + +def _raise_error(msg): + raise AnsibleFilterError(msg) + + +def vlan_parser(data, first_line_len=48, other_line_len=44): + """ + Input: Unsorted list of vlan integers + Output: Sorted string list of integers according to IOS-like vlan list rules + + 1. Vlans are listed in ascending order + 2. Runs of 3 or more consecutive vlans are listed with a dash + 3. The first line of the list can be first_line_len characters long + 4. Subsequent list lines can be other_line_len characters + """ + if not isinstance(data, (list)): + _raise_error("Input is not valid for vlan_parser") + # Sort and remove duplicates + sorted_list = sorted(set(data)) + + if sorted_list[0] < 1 or sorted_list[-1] > 4094: + _raise_error("Valid VLAN range is 1-4094") + + parse_list = [] + idx = 0 + while idx < len(sorted_list): + start = idx + end = start + while end < len(sorted_list) - 1: + if sorted_list[end + 1] - sorted_list[end] == 1: + end += 1 + else: + break + + if start == end: + # Single VLAN + parse_list.append(str(sorted_list[idx])) + elif start + 1 == end: + # Run of 2 VLANs + parse_list.append(str(sorted_list[start])) + parse_list.append(str(sorted_list[end])) + else: + # Run of 3 or more VLANs + parse_list.append(str(sorted_list[start]) + "-" + str(sorted_list[end])) + idx = end + 1 + + line_count = 0 + result = [""] + for vlans in parse_list: + # First line (" switchport trunk allowed vlan ") + if line_count == 0: + if len(result[line_count] + vlans) > first_line_len: + result.append("") + line_count += 1 + result[line_count] += vlans + "," + else: + result[line_count] += vlans + "," + + # Subsequent lines (" switchport trunk allowed vlan add ") + else: + if len(result[line_count] + vlans) > other_line_len: + result.append("") + line_count += 1 + result[line_count] += vlans + "," + else: + result[line_count] += vlans + "," + + # Remove trailing orphan commas + for idx in range(0, len(result)): + result[idx] = result[idx].rstrip(",") + + # Sometimes text wraps to next line, but there are no remaining VLANs + if "" in result: + result.remove("") + + return result diff --git a/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/content_templates_parser.py b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/content_templates_parser.py new file mode 100644 index 0000000..5ae051e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/content_templates_parser.py @@ -0,0 +1,78 @@ +""" +content templates parser + +This is the content templates parser for use with the cli_parse module and action plugin. +The parser functionality used by the network resource modules is leveraged here. + +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ + author: Rohit Thakur (@rohitthakur2590) + name: content_templates + short_description: Define configurable options for C(content) sub-plugin of C(cli_parse) module + description: + - This plugin documentation provides the configurable options that can be passed + to the I(ansible.utils.cli_parse) plugins when I(ansible.netcommon.content_templates) is used as a value for + I(name) option. + version_added: 1.0.0 +""" + +EXAMPLES = """ +- name: "Run command and parse with content_templates" + ansible.utils.cli_parse: + command: "show bgp summary" + parser: + name: ansible.netcommon.content_templates + set_fact: bgp_summary + register: ios_bgp_health + +""" +from ansible.module_utils.common.text.converters import to_native +from ansible_collections.ansible.utils.plugins.plugin_utils.base.cli_parser import CliParserBase + +from ansible_collections.ansible.netcommon.plugins.module_utils.cli_parser.cli_parsertemplate import ( + CliParserTemplate, +) + + +class CliParser(CliParserBase): + """The content_templates parser class + Convert raw text to structured data using the resource module parser + """ + + DEFAULT_TEMPLATE_EXTENSION = "yaml" + PROVIDE_TEMPLATE_CONTENTS = True + + def parse(self, *_args, **kwargs): + """Std entry point for a cli_parse parse execution + + :return: Errors or parsed text as structured data + :rtype: dict + + :example: + + The parse function of a parser should return a dict: + {"errors": [a list of errors]} + or + {"parsed": obj} + """ + + template_contents = kwargs["template_contents"] + parser = CliParserTemplate(lines=self._task_args.get("text", "").splitlines()) + try: + template_obj = list(eval(template_contents)) + except Exception as exc: + return {"errors": [to_native(exc)]} + + try: + parser.PARSERS = template_obj + out = {"parsed": parser.parse()} + return out + except Exception as exc: + msg = "An error occurred during content_templates parsing. Error: {err}" + return {"errors": [msg.format(err=to_native(exc))]} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/native_parser.py b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/native_parser.py new file mode 100644 index 0000000..2456e69 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/native_parser.py @@ -0,0 +1,102 @@ +# 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 + +""" +native parser + +This is the native parser for use with the cli_parse module and action plugin. +The parser functionality used by the network resource modules is leveraged here. + +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ + author: Bradley Thornton (@cidrblock) + name: native + short_description: Define configurable options for C(native) sub-plugin of C(cli_parse) module + description: + - This plugin documentation provides the configurable options that can be passed + to the I(ansible.utils.cli_parse) plugins when I(ansible.netcommon.native) is used as a value for + I(name) option. + version_added: 1.0.0 +""" + +EXAMPLES = r""" +- name: "Run command and parse with native" + ansible.utils.cli_parse: + command: "show interface" + parser: + name: ansible.netcommon.native + set_fact: POpqMQoJWTiDpEW + register: nxos_native_command + +- name: "Pass text and template_path" + ansible.utils.cli_parse: + text: "{{ nxos_native_command['stdout'] }}" + parser: + name: ansible.netcommon.native + template_path: "/home/user/templates/nxos_show_interface.yaml" + register: nxos_native_text +""" + +from ansible.module_utils.common.text.converters import to_native +from ansible_collections.ansible.utils.plugins.plugin_utils.base.cli_parser import CliParserBase + +from ansible_collections.ansible.netcommon.plugins.module_utils.cli_parser.cli_parsertemplate import ( + CliParserTemplate, +) + + +try: + import yaml + + try: + from yaml import CSafeLoader as SafeLoader + except ImportError: + from yaml import SafeLoader + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +class CliParser(CliParserBase): + """The native parser class + Convert raw text to structured data using the resource module parser + """ + + DEFAULT_TEMPLATE_EXTENSION = "yaml" + PROVIDE_TEMPLATE_CONTENTS = True + + def parse(self, *_args, **kwargs): + """Std entry point for a cli_parse parse execution + + :return: Errors or parsed text as structured data + :rtype: dict + + :example: + + The parse function of a parser should return a dict: + {"errors": [a list of errors]} + or + {"parsed": obj} + """ + # res = self._check_reqs() + # if res.get("errors"): + # return res + + template_contents = kwargs["template_contents"] + parser = CliParserTemplate(lines=self._task_args.get("text", "").splitlines()) + try: + template_obj = yaml.load(template_contents, SafeLoader) + except Exception as exc: + return {"errors": [to_native(exc)]} + + try: + parser.PARSERS = template_obj + return {"parsed": parser.parse()} + except Exception as exc: + msg = "Native parser returned an error while parsing. Error: {err}" + return {"errors": [msg.format(err=to_native(exc))]} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/ntc_templates_parser.py b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/ntc_templates_parser.py new file mode 100644 index 0000000..ba04d55 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/ntc_templates_parser.py @@ -0,0 +1,126 @@ +# 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 + +""" +ntc_templates parser + +This is the ntc_templates parser for use with the cli_parse module and action plugin. +https://github.com/networktocode/ntc-templates + +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ + author: Bradley Thornton (@cidrblock) + name: ntc_templates + short_description: Define configurable options for C(ntc_templates) sub-plugin of C(cli_parse) module + description: + - This plugin documentation provides the configurable options that can be passed + to the I(ansible.utils.cli_parse) plugins when I(ansible.netcommon.ntc_templates) is used as a value for + I(name) option. + version_added: 1.0.0 +""" + +EXAMPLES = r""" +- name: "Run command and parse with ntc_templates" + ansible.utils.cli_parse: + command: "show interface" + parser: + name: ansible.netcommon.ntc_templates + register: nxos_ntc_templates_command + +- name: "Pass text and command" + ansible.utils.cli_parse: + text: "{{ nxos_ntc_templates_command['stdout'] }}" + parser: + name: ansible.netcommon.ntc_templates + command: show interface + register: nxos_ntc_templates_text +""" + +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native +from ansible_collections.ansible.utils.plugins.plugin_utils.base.cli_parser import CliParserBase + + +try: + from ntc_templates.parse import parse_output + + HAS_NTC = True +except ImportError: + HAS_NTC = False + +ANSIBLE_NETWORK_OS = { + "ios": "cisco_ios", + "iosxr": "cisco_xr", + "nxos": "cisco_nxos", + "asa": "cisco_asa", + "eos": "arista_eos", + "junos": "juniper_junos", +} + + +class CliParser(CliParserBase): + """The ntc_templates parser class + Convert raw text to structured data using textfsm and predefined templates in + the ntc-templates python package + """ + + DEFAULT_TEMPLATE_EXTENSION = None + PROVIDE_TEMPLATE_CONTENTS = False + + def _check_reqs(self): + """Check the prerequisites for the ntc template parser + + :return: A dict with errors or a network_os and command + :rtype: dict + """ + errors = [] + + if not HAS_NTC: + errors.append(missing_required_lib("ntc-templates")) + + network_os = self._task_args.get("parser").get("os") + if network_os: + self._debug("OS set to {os} using task args".format(os=network_os)) + if not network_os: + ano = self._task_vars.get("ansible_network_os", "").split(".")[-1] + network_os = ANSIBLE_NETWORK_OS[ano] + self._debug("OS set to {os} using ansible_network_os".format(os=network_os)) + if not network_os: + errors.append("Either 'parser/os' needs to be specified or 'ansible_network_os' set.") + command = self._task_args.get("parser").get("command") + if not command: + errors.append("'command' needs to be specified.") + + if errors: + return {"errors": errors} + return {"network_os": network_os, "command": command} + + def parse(self, *_args, **_kwargs): + """Std entry point for a cli_parse parse execution + + :return: Errors or parsed text as structured data + :rtype: dict + + :example: + + The parse function of a parser should return a dict: + {"errors": [a list of errors]} + or + {"parsed": obj} + """ + cli_output = self._task_args.get("text") + res = self._check_reqs() + if res.get("errors"): + return {"errors": res.get("errors")} + platform = res["network_os"] + command = res["command"] + try: + parsed = parse_output(platform=platform, command=command, data=cli_output) + return {"parsed": parsed} + except Exception as exc: + return {"errors": [to_native(exc)]} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/pyats_parser.py b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/pyats_parser.py new file mode 100644 index 0000000..bca7db8 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/cli_parser/pyats_parser.py @@ -0,0 +1,141 @@ +# 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 + +""" +pyats parser + +This is the pyats parser for use with the cli_parse module and action plugin. +https://developer.cisco.com/docs/pyats/#!parsing-device-output + +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ + author: Bradley Thornton (@cidrblock) + name: pyats + short_description: Define configurable options for C(pyats) sub-plugin of C(cli_parse) module + description: + - This plugin documentation provides the configurable options that can be passed + to the I(ansible.utils.cli_parse) plugins when I(ansible.netcommon.pyats) is used as a value for + I(name) option. + version_added: 1.0.0 +""" + +EXAMPLES = r""" +- name: "Run command and parse with pyats" + ansible.utils.cli_parse: + command: "show interface" + parser: + name: ansible.netcommon.pyats + register: nxos_pyats_command + +- name: "Pass text and command" + ansible.utils.cli_parse: + text: "{{ nxos_pyats_command['stdout'] }}" + parser: + name: ansible.netcommon.pyats + command: show interface + register: nxos_pyats_text +""" + +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_native +from ansible_collections.ansible.utils.plugins.plugin_utils.base.cli_parser import CliParserBase + + +try: + from genie.conf.base import Device + + HAS_GENIE = True +except ImportError: + HAS_GENIE = False + +try: + from pyats.datastructures import AttrDict + + HAS_PYATS = True +except ImportError: + HAS_PYATS = False + + +class CliParser(CliParserBase): + """The pyats parser class + Convert raw text to structured data using pyats/genie + """ + + DEFAULT_TEMPLATE_EXTENSION = None + PROVIDE_TEMPLATE_CONTENTS = False + + @staticmethod + def _check_reqs(): + """Check the prerequisites are installed for pyats/genie + + :return dict: A dict with a list of errors + """ + errors = [] + if not HAS_GENIE: + errors.append(missing_required_lib("genie")) + if not HAS_PYATS: + errors.append(missing_required_lib("pyats")) + return errors + + def _check_vars(self): + """Ensure specific args are set + + :return: A dict with a list of errors + :rtype: dict + """ + errors = [] + if not self._task_args.get("parser").get("command"): + errors.append("The pyats parser requires parser/command be provided.") + return errors + + def _transform_ansible_network_os(self): + """Transform the ansible_network_os to a pyats OS + The last part of the fully qualified name is used + org.name.platform => platform + + In the case of ios, the os is assumed to be iosxe + """ + ane = self._task_vars.get("ansible_network_os", "").split(".")[-1] + if ane == "ios": + self._debug("ansible_network_os was ios, using iosxe.") + ane = "iosxe" + self._debug("OS set to '{ane}' using 'ansible_network_os'.".format(ane=ane)) + return ane + + def parse(self, *_args, **_kwargs): + """Std entry point for a cli_parse parse execution + + :return: Errors or parsed text as structured data + :rtype: dict + + :example: + + The parse function of a parser should return a dict: + {"errors": [a list of errors]} + or + {"parsed": obj} + """ + errors = self._check_reqs() + errors.extend(self._check_vars()) + if errors: + return {"errors": errors} + + command = self._task_args.get("parser").get("command") + network_os = self._task_args.get("parser").get("os") or self._transform_ansible_network_os() + cli_output = self._task_args.get("text") + + device = Device("new_device", os=network_os) + device.custom.setdefault("abstraction", {})["order"] = ["os"] + device.cli = AttrDict({"execute": None}) + + try: + parsed = device.parse(command, output=cli_output) + except Exception as exc: + msg = "The pyats library return an error for '{cmd}' for '{os}'. Error: {err}." + return {"errors": [msg.format(cmd=command, os=network_os, err=to_native(exc))]} + return {"parsed": parsed} diff --git a/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/grpc/base.py b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/grpc/base.py new file mode 100644 index 0000000..78b5548 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/plugins/sub_plugins/grpc/base.py @@ -0,0 +1,68 @@ +# (c) 2022 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 + +from functools import wraps + +from ansible.plugins import AnsiblePlugin + + +def ensure_connect(func): + @wraps(func) + def wrapped(self, *args, **kwargs): + if not self._connection._connected: + self._connection._connect() + return func(self, *args, **kwargs) + + return wrapped + + +class GrpcBase(AnsiblePlugin): + """ + A base class for implementing gRPC abstraction layer + """ + + __rpc__ = ["channel", "get_config", "edit_config", "get"] + + def __init__(self, connection): + super(GrpcBase, self).__init__() + self._connection = connection + + @property + @ensure_connect + def channel(self): + return self._connection._channel + + def get_config(self, section=None): + """ + Retrieve all or part of a specified configuration + (by default entire configuration is retrieved). + :param section: This argument specifies the portion of the configuration data to retrieve + :return: Returns the response received from gRPC server from target host in string format + """ + pass + + def get(self, section=None): + """ + Retrieve device state information. + :param section: This argument specifies the portion of the state data to retrieve + (by default entire state data is retrieved) + :return: Returns the json string as a response + """ + pass + + def edit_config(self, config=None, action=None): + """ + Loads all or part of the specified *config* to the configuration datastore. + :param config: The configuration that needs to be push on target host + :param action: The action to be performed on the configuration datastore for example: 'merge', + 'replace', 'delete' etc. + :return: Returns the response received from gRPC server from target host in string format + """ + pass diff --git a/collections/ansible_collections/ansible/netcommon/pyproject.toml b/collections/ansible_collections/ansible/netcommon/pyproject.toml new file mode 100644 index 0000000..7f7d132 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/pyproject.toml @@ -0,0 +1,9 @@ +[tool.black] +line-length = 100 +target-version = ["py36"] +force-exclude = "compat" + +[tool.pytest.ini_options] +addopts = ["-vvv", "-n", "2", "--log-level", "WARNING", "--color", "yes"] +testpaths = ["tests"] +filterwarnings = ['ignore:AnsibleCollectionFinder has already been configured'] diff --git a/collections/ansible_collections/ansible/netcommon/requirements.txt b/collections/ansible_collections/ansible/netcommon/requirements.txt new file mode 100644 index 0000000..df22a47 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/requirements.txt @@ -0,0 +1,7 @@ +ansible-pylibssh >= 1.4.0 +jxmlease +ncclient +paramiko +xmltodict +grpcio +protobuf diff --git a/collections/ansible_collections/ansible/netcommon/test-requirements.txt b/collections/ansible_collections/ansible/netcommon/test-requirements.txt new file mode 100644 index 0000000..785c2c1 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/test-requirements.txt @@ -0,0 +1,20 @@ +# For ansible-tox-linters +black==23.3.0 +flake8 +yamllint + +# Unit test runner +pytest-ansible +pytest-xdist +pytest-cov + +# The following are 3rd party libs for cli_parse +ntc_templates +pyats +genie +passlib + +# for integration tests +jxmlease +ncclient>=0.7.0 +ssh-python diff --git a/collections/ansible_collections/ansible/netcommon/tests/.gitignore b/collections/ansible_collections/ansible/netcommon/tests/.gitignore new file mode 100644 index 0000000..ea1472e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/collections/ansible_collections/ansible/netcommon/tests/config.yml b/collections/ansible_collections/ansible/netcommon/tests/config.yml new file mode 100644 index 0000000..6fe2c43 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/config.yml @@ -0,0 +1,3 @@ +--- +modules: + python_requires: ">=3.10" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/defaults/main.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/defaults/main.yaml new file mode 100644 index 0000000..5f709c5 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/meta/main.yml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/meta/main.yml new file mode 100644 index 0000000..48f2760 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: prepare_iosxr_tests + when: ansible_network_os == 'cisco.iosxr.iosxr' diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/iosxr.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/iosxr.yaml new file mode 100644 index 0000000..bec3926 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/iosxr.yaml @@ -0,0 +1,17 @@ +--- +- name: Collect all grpc test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/iosxr" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=ansible.netcommon.grpc) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/junos.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/junos.yaml new file mode 100644 index 0000000..2acecd7 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/junos.yaml @@ -0,0 +1,17 @@ +--- +- name: Collect all grpc test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/junos" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=ansible.netcommon.grpc) + ansible.builtin.include_tasks: "{{ test_case_to_run }} ansible_connection=ansible.netcommon.grpc" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/main.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/main.yaml new file mode 100644 index 0000000..ed49a6e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/main.yaml @@ -0,0 +1,18 @@ +--- +- name: Run junos gRPC tests + ansible.builtin.include_tasks: junos.yaml + when: ansible_network_os == 'junipernetworks.junos.junos' + tags: + - grpc + +- name: Run iosxr gRPC tests + ansible.builtin.include_tasks: iosxr.yaml + when: ansible_network_os == 'cisco.iosxr.iosxr' + tags: + - grpc + +- name: Run nxos gRPC tests + ansible.builtin.include_tasks: nxos.yaml + when: ansible_network_os == 'cisco.nxos.nxos' + tags: + - grpc diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/nxos.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/nxos.yaml new file mode 100644 index 0000000..8b811f4 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tasks/nxos.yaml @@ -0,0 +1,17 @@ +--- +- name: Collect all grpc test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/nxos" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=ansible.netcommon.grpc) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/basic.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/basic.yaml new file mode 100644 index 0000000..e104132 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/basic.yaml @@ -0,0 +1,144 @@ +--- +- debug: msg="START grpc_config iosxr/basic.yaml on connection=ansible.netcommon.grpc" + +- name: setup + connection: ansible.netcommon.network_cli + cisco.iosxr.iosxr_config: + commands: + - no router bgp + - no router static address-family ipv4 unicast 1.2.3.6/32 10.0.2.2 + +- name: Sleep for 30 seconds and continue with play + wait_for: + timeout: 30 + delegate_to: localhost + +- name: save config test + register: result + connection: ansible.netcommon.grpc + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_config: + backup: true + +- assert: + that: + - "'backup_path' in result" + +- name: Merge static route config + register: result + connection: ansible.netcommon.grpc + diff: true + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_config: + config: + Cisco-IOS-XR-ip-static-cfg:router-static: + default-vrf: + address-family: + vrfipv4: + vrf-unicast: + vrf-prefixes: + vrf-prefix: + - prefix: "1.2.3.6" + prefix-length: 32 + vrf-route: + vrf-next-hop-table: + vrf-next-hop-next-hop-address: + - next-hop-address: "10.0.2.2" + + state: merged + +- assert: + that: + - result.changed == true + +- name: Assert that diff dicts are correctly generated + assert: + that: + - result.diff['before'] == before + - result.diff['after'] == after + +- name: get running interface confiugration + register: result + connection: ansible.netcommon.grpc + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_get: + command: "show running-config" + display: "text" + +- assert: + that: + - "'router static' in result.stdout" + - "'1.2.3.6/32 10.0.2.2' in result.stdout" + +- name: Replace bgp config + register: result + connection: ansible.netcommon.grpc + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_config: + config: "{{ lookup('file', './fixtures/bgp_start.yml') }}" + state: replaced + +- assert: + that: + - result.changed == true + +- name: get running interface confiugration + register: result + connection: ansible.netcommon.grpc + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_get: + command: "show running-config router bgp" + display: "text" + +- assert: + that: + - "'router bgp 65400' in result.stdout" + +- name: Delete bgp config + register: result + connection: ansible.netcommon.grpc + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_config: + config: "{{ lookup('file', './fixtures/bgp_delete.yml') }}" + state: deleted + +- assert: + that: + - result.changed == true + +- name: get running interface confiugration + register: result + connection: ansible.netcommon.grpc + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_get: + command: "show running-config router bgp" + display: "text" + +- assert: + that: + - "'router bgp 65400' not in result.stdout" + +- name: setup - teardown + connection: ansible.netcommon.network_cli + cisco.iosxr.iosxr_config: + commands: + - no address-family ipv4 unicast 1.2.3.6/32 10.0.2.2 + parents: + - router static + match: none + +- debug: msg="END grpc_config iosxr/basic.yaml on connection=ansible.netcommon.grpc" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/fixtures/bgp_delete.yml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/fixtures/bgp_delete.yml new file mode 100644 index 0000000..9e7fac9 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/fixtures/bgp_delete.yml @@ -0,0 +1,8 @@ +--- +Cisco-IOS-XR-ipv4-bgp-cfg:bgp: + instance: + - instance-name: default + instance-as: + - as: 0 + four-byte-as: + - as: 65400 diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/fixtures/bgp_start.yml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/fixtures/bgp_start.yml new file mode 100644 index 0000000..c2ea42d --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/tests/iosxr/fixtures/bgp_start.yml @@ -0,0 +1,35 @@ +--- +Cisco-IOS-XR-ipv4-bgp-cfg:bgp: + instance: + - instance-name: default + instance-as: + - as: 0 + four-byte-as: + - as: 65400 + bgp-running: + - + default-vrf: + global: + router-id: 12.1.1.10 + global-afs: + global-af: + - af-name: ipv4-unicast + enable: + - + sourced-networks: + sourced-network: + - network-addr: 11.1.1.0 + network-prefix: 24 + bgp-entity: + neighbors: + neighbor: + - neighbor-address: 11.1.1.20 + remote-as: + as-xx: 0 + as-yy: 65450 + neighbor-afs: + neighbor-af: + - af-name: ipv4-unicast + activate: + - + next-hop-self: true diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/vars/main.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/vars/main.yaml new file mode 100644 index 0000000..507076d --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_config/vars/main.yaml @@ -0,0 +1,37 @@ +--- +before: + Cisco-IOS-XR-ip-static-cfg:router-static: + default-vrf: + address-family: + vrfipv4: + vrf-unicast: + vrf-prefixes: + vrf-prefix: + - prefix: "0.0.0.0" + prefix-length: 0 + vrf-route: + vrf-next-hop-table: + vrf-next-hop-interface-name-next-hop-address: + - interface-name: "MgmtEth0/RP0/CPU0/0" + next-hop-address: "10.0.151.254" +after: + Cisco-IOS-XR-ip-static-cfg:router-static: + default-vrf: + address-family: + vrfipv4: + vrf-unicast: + vrf-prefixes: + vrf-prefix: + - prefix: "0.0.0.0" + prefix-length: 0 + vrf-route: + vrf-next-hop-table: + vrf-next-hop-interface-name-next-hop-address: + - interface-name: "MgmtEth0/RP0/CPU0/0" + next-hop-address: "10.0.151.254" + - prefix: "1.2.3.6" + prefix-length: 32 + vrf-route: + vrf-next-hop-table: + vrf-next-hop-next-hop-address: + - next-hop-address: "10.0.2.2" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/defaults/main.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/defaults/main.yaml new file mode 100644 index 0000000..5f709c5 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/meta/main.yml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/meta/main.yml new file mode 100644 index 0000000..48f2760 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: prepare_iosxr_tests + when: ansible_network_os == 'cisco.iosxr.iosxr' diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tasks/iosxr.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tasks/iosxr.yaml new file mode 100644 index 0000000..bec3926 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tasks/iosxr.yaml @@ -0,0 +1,17 @@ +--- +- name: Collect all grpc test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/iosxr" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=ansible.netcommon.grpc) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tasks/main.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tasks/main.yaml new file mode 100644 index 0000000..c59dd45 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tasks/main.yaml @@ -0,0 +1,6 @@ +--- +- name: Run iosxr gRPC tests + ansible.builtin.include_tasks: iosxr.yaml + when: ansible_network_os == 'cisco.iosxr.iosxr' + tags: + - grpc diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tests/iosxr/basic.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tests/iosxr/basic.yaml new file mode 100644 index 0000000..22291f2 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/hold/grpc_get/tests/iosxr/basic.yaml @@ -0,0 +1,57 @@ +--- +- debug: msg="START grpc_get iosxr/basic.yaml on connection=ansible.netcommon.grpc" + +- name: setup interface + connection: ansible.netcommon.network_cli + cisco.iosxr.iosxr_config: + commands: + - description this is test interface Loopback999 + - no shutdown + parents: + - interface Loopback999 + match: none + +- name: get running interface confiugration + register: result + connection: ansible.netcommon.grpc + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_get: + command: "show running-config" + display: "text" + +- assert: + that: + - "'description this is test interface Loopback999' in result.stdout" + +- name: get interface details using section + register: result + connection: ansible.netcommon.grpc + vars: + ansible_grpc_connection_type: cisco.iosxr.grpc + ansible_port: 57777 + ansible.netcommon.grpc_get: + section: + Cisco-IOS-XR-ifmgr-cfg:interface-configurations: + - null + +- name: Assert interface config + assert: + that: + - "item['description'] == 'this is test interface Loopback999'" + loop: "{{ result.output[0]['Cisco-IOS-XR-ifmgr-cfg:interface-configurations']['interface-configuration'] }}" + when: item['interface-name'] == "Loopback999" + +- name: setup - teardown + connection: ansible.netcommon.network_cli + cisco.iosxr.iosxr_config: + commands: + - no description + - shutdown + parents: + - interface Loopback999 + match: none + +- debug: msg="END grpc_get iosxr/basic.yaml on connection={{ ansible_connection + }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/labs/inventory.j2 b/collections/ansible_collections/ansible/netcommon/tests/integration/labs/inventory.j2 new file mode 100644 index 0000000..1eed91e --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/labs/inventory.j2 @@ -0,0 +1,13 @@ +[cli] +nxos ansible_host={{ ansible_host }} ansible_user=cisco ansible_password=cisco ansible_connection=ansible.netcommon.network_cli ansible_network_os=cisco.nxos.nxos ansible_ssh_port={{ nxos_ssh_port }} ansible_network_cli_ssh_type={{ ansible_network_cli_ssh_type }} +ubuntu ansible_host={{ ansible_host }} ansible_user=cisco ansible_password=cisco ansible_ssh_port={{ ubuntu_ssh_port }} + +[netconf] +nxos ansible_host={{ ansible_host }} ansible_user=cisco ansible_password=cisco ansible_network_os=cisco.nxos.nxos ansible_ssh_port={{ nxos_ssh_port }} +iosxr ansible_host={{ ansible_host }} ansible_user=ansible ansible_password=ansible ansible_network_os=cisco.iosxr.iosxr ansible_ssh_port={{ iosxr_ssh_port }} + +[restconf] +iosxe ansible_host={{ ansible_host }} ansible_user=ansible ansible_password=ansible ansible_connection=ansible.netcommon.httpapi ansible_network_os=ansible.netcommon.restconf ansible_httpapi_use_ssl=true ansible_httpapi_validate_certs=false ansible_httpapi_port={{ iosxe_http_port }} ansible_ssh_port={{ iosxe_ssh_port }} + +[httpapi] +nxos ansible_host={{ ansible_host }} ansible_user=cisco ansible_password=cisco ansible_connection=ansible.netcommon.network_cli ansible_network_os=cisco.nxos.nxos ansible_ssh_port={{ nxos_ssh_port }} ansible_httpapi_port={{ nxos_http_port }} diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/labs/multi.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/labs/multi.yaml new file mode 100644 index 0000000..9fe4199 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/labs/multi.yaml @@ -0,0 +1,649 @@ +--- +annotations: + - border_color: "#472B2B" + border_radius: 0 + border_style: "" + color: "#203130FF" + thickness: 1 + type: rectangle + x1: -1160.0 + y1: -560.0 + x2: 400.0 + y2: 640.0 + z_index: 0 + - border_color: "#808080FF" + border_radius: 0 + border_style: 4,2 + color: "#7393CC87" + thickness: 1 + type: rectangle + x1: -760.0 + y1: -560.0 + x2: 800.0 + y2: 640.0 + z_index: 1 + - border_color: "#808080FF" + border_radius: 0 + border_style: "" + color: "#FFFFFFA6" + thickness: 1 + type: rectangle + x1: 40.0 + y1: -560.0 + x2: 400.0 + y2: 640.0 + z_index: 2 + - border_color: "#00000000" + border_style: "" + color: "#110606" + rotation: 0 + text_bold: false + text_content: |- + Ansible netcommon automation upstream tests for CML. + iosxr - needed for netconf + nxos - needed for network_cli and httpapi (netconf is disabled) + ios - needed for restconf and network_cli (grpc on hold) + ubuntu - needed for jump host and parser tests + text_font: monospace + text_italic: false + text_size: 12 + text_unit: pt + thickness: 1 + type: text + x1: -600.0 + y1: -480.0 + z_index: 3 +nodes: + - boot_disk_size: null + configuration: + - name: nxos_config.txt + content: |- + !Command: show running-config + !Running configuration last done at: Tue Nov 18 07:12:09 2025 + !Time: Tue Nov 18 07:23:05 2025 + + version 10.4(2) Bios:version + hostname testnxos + vdc testnxos id 1 + limit-resource vlan minimum 16 maximum 4094 + limit-resource vrf minimum 2 maximum 4096 + limit-resource port-channel minimum 0 maximum 511 + limit-resource m4route-mem minimum 58 maximum 58 + limit-resource m6route-mem minimum 8 maximum 8 + + feature nxapi + feature netconf + + no password strength-check + username admin password 5 $5$AADOBF$GVXUV2cvmlZH42xDu6wnqfBpV9gXF61WGVNrGF5Gk6C role network-admin + username cisco password 5 $5$JDAGKP$xq8ldgUvOKxCXVFRVZwPoxbG7/qFnmWJ9UoiUpZuZwD role network-admin + username cisco passphrase lifetime 99999 warntime 14 gracetime 3 + username ansible password 5 $5$GHBNKA$8ccD8KWjmP6NzxzkIRufl7giaUWjrhAn4SRrVWYZ4R1 role network-admin + username ansible passphrase lifetime 99999 warntime 14 gracetime 3 + ip domain-lookup + snmp-server user admin network-admin auth md5 040F90283AEB9A92CCAF81F5CDA2639FFFCD priv aes-128 006084272498F8CD8F98DE888DEC6899C6D0 localizedV2key + snmp-server user cisco network-admin auth md5 495336A9A9C95E642E4E27331C80964B2464 priv aes-128 005122F7FA9646642534607B1AB8DF123F76 localizedV2key + snmp-server user ansible network-admin auth md5 5207BA419F48B4EB39149FDC00BE411BC828 priv aes-128 041EED4A876CACFD7656EA885CE15D3CE108 localizedV2key + rmon event 1 log trap public description FATAL(1) owner PMON@FATAL + rmon event 2 log trap public description CRITICAL(2) owner PMON@CRITICAL + rmon event 3 log trap public description ERROR(3) owner PMON@ERROR + rmon event 4 log trap public description WARNING(4) owner PMON@WARNING + rmon event 5 log trap public description INFORMATION(5) owner PMON@INFO + + vlan 1 + + vrf context management + ip name-server 192.168.255.1 + ip route 0.0.0.0/0 192.168.255.1 + nxapi http port 80 + + + interface Ethernet1/1 + description Configured by CI automation + + interface Ethernet1/2 + + interface Ethernet1/3 + + interface Ethernet1/4 + + interface Ethernet1/5 + + interface Ethernet1/6 + + interface Ethernet1/7 + + interface Ethernet1/8 + + interface Ethernet1/9 + + interface Ethernet1/10 + + interface Ethernet1/11 + + interface Ethernet1/12 + + interface Ethernet1/13 + + interface Ethernet1/14 + + interface Ethernet1/15 + + interface Ethernet1/16 + + interface Ethernet1/17 + + interface Ethernet1/18 + + interface Ethernet1/19 + + interface Ethernet1/20 + + interface Ethernet1/21 + + interface Ethernet1/22 + + interface Ethernet1/23 + + interface Ethernet1/24 + + interface Ethernet1/25 + + interface Ethernet1/26 + + interface Ethernet1/27 + + interface Ethernet1/28 + + interface Ethernet1/29 + + interface Ethernet1/30 + + interface Ethernet1/31 + + interface Ethernet1/32 + + interface Ethernet1/33 + + interface Ethernet1/34 + + interface Ethernet1/35 + + interface Ethernet1/36 + + interface Ethernet1/37 + + interface Ethernet1/38 + + interface Ethernet1/39 + + interface Ethernet1/40 + + interface Ethernet1/41 + + interface Ethernet1/42 + + interface Ethernet1/43 + + interface Ethernet1/44 + + interface Ethernet1/45 + + interface Ethernet1/46 + + interface Ethernet1/47 + + interface Ethernet1/48 + + interface Ethernet1/49 + + interface Ethernet1/50 + + interface Ethernet1/51 + + interface Ethernet1/52 + + interface Ethernet1/53 + + interface Ethernet1/54 + + interface Ethernet1/55 + + interface Ethernet1/56 + + interface Ethernet1/57 + + interface Ethernet1/58 + + interface Ethernet1/59 + + interface Ethernet1/60 + + interface Ethernet1/61 + + interface Ethernet1/62 + + interface Ethernet1/63 + + interface Ethernet1/64 + + interface mgmt0 + ip address dhcp + vrf member management + + interface loopback888 + description test for ansible + shutdown + icam monitor scale + + line console + exec-timeout 0 + line vty + exec-timeout 0 + event manager applet BOOTCONFIG + event syslog pattern "Configured from vty" + action 1.0 cli python bootflash:set_boot.py + + no logging console + cpu_limit: null + cpus: null + data_volume: null + hide_links: false + id: n0 + image_definition: null + label: nxos + node_definition: nxosv9000 + parameters: {} + ram: null + tags: [] + x: -1040 + y: -240 + interfaces: + - id: i0 + label: Loopback0 + type: loopback + - id: i1 + label: mgmt0 + slot: 0 + type: physical + - id: i2 + label: Ethernet1/1 + slot: 1 + type: physical + - id: i3 + label: Ethernet1/2 + slot: 2 + type: physical + - id: i4 + label: Ethernet1/3 + slot: 3 + type: physical + - boot_disk_size: null + configuration: + - name: iosxr_config.txt + content: |- + username ansible + group sysadmin + group root-lr + secret 0 ansible + ! + hostname iosxr + interface MgmtEth0/RP0/CPU0/0 + ipv4 address dhcp + no shutdown + ! + interface Loopback888 + ! + interface Loopback999 + ! + interface GigabitEthernet0/0/0/0 + shutdown + ! + router static + address-family ipv4 unicast + 0.0.0.0/0 192.168.255.1 + ! + ! + ssh timeout 100 + ssh server v2 + netconf-yang agent + ssh + ! + cpu_limit: null + cpus: null + data_volume: null + hide_links: false + id: n1 + image_definition: null + label: iosxr + node_definition: iosxrv9000 + parameters: {} + ram: null + tags: [] + x: -1040 + y: -400 + interfaces: + - id: i0 + label: Loopback0 + type: loopback + - id: i1 + label: MgmtEth0/RP0/CPU0/0 + slot: 0 + type: physical + - id: i2 + label: donotuse1 + slot: 1 + type: physical + - id: i3 + label: donotuse2 + slot: 2 + type: physical + - id: i4 + label: GigabitEthernet0/0/0/0 + slot: 3 + type: physical + - boot_disk_size: null + configuration: + - name: iosxe_config.txt + content: |- + service timestamps debug datetime msec + service timestamps log datetime msec + platform qfp utilization monitor load 80 + platform punt-keepalive disable-kernel-core + platform sslvpn use-pd + platform console serial + ! + hostname cisco + ! + boot-start-marker + boot-end-marker + ! + ! + no aaa new-model + ! + ! + ! + ! + ! + ! + ! + ! + ! + ! + ! + ! + ip domain name ansible.com + ! + ! + ! + login on-success log + ! + ! + subscriber templating + ! + pae + ! + ! + crypto pki trustpoint SLA-TrustPoint + enrollment pkcs12 + revocation-check crl + hash sha256 + ! + ! + crypto pki certificate chain SLA-TrustPoint + certificate ca 01 + 30820321 30820209 A0030201 02020101 300D0609 2A864886 F70D0101 0B050030 + 32310E30 0C060355 040A1305 43697363 6F312030 1E060355 04031317 43697363 + 6F204C69 63656E73 696E6720 526F6F74 20434130 1E170D31 33303533 30313934 + 3834375A 170D3338 30353330 31393438 34375A30 32310E30 0C060355 040A1305 + 43697363 6F312030 1E060355 04031317 43697363 6F204C69 63656E73 696E6720 + 526F6F74 20434130 82012230 0D06092A 864886F7 0D010101 05000382 010F0030 + 82010A02 82010100 A6BCBD96 131E05F7 145EA72C 2CD686E6 17222EA1 F1EFF64D + CBB4C798 212AA147 C655D8D7 9471380D 8711441E 1AAF071A 9CAE6388 8A38E520 + 1C394D78 462EF239 C659F715 B98C0A59 5BBB5CBD 0CFEBEA3 700A8BF7 D8F256EE + 4AA4E80D DB6FD1C9 60B1FD18 FFC69C96 6FA68957 A2617DE7 104FDC5F EA2956AC + 7390A3EB 2B5436AD C847A2C5 DAB553EB 69A9A535 58E9F3E3 C0BD23CF 58BD7188 + 68E69491 20F320E7 948E71D7 AE3BCC84 F10684C7 4BC8E00F 539BA42B 42C68BB7 + C7479096 B4CB2D62 EA2F505D C7B062A4 6811D95B E8250FC4 5D5D5FB8 8F27D191 + C55F0D76 61F9A4CD 3D992327 A8BB03BD 4E6D7069 7CBADF8B DF5F4368 95135E44 + DFC7C6CF 04DD7FD1 02030100 01A34230 40300E06 03551D0F 0101FF04 04030201 + 06300F06 03551D13 0101FF04 05300301 01FF301D 0603551D 0E041604 1449DC85 + 4B3D31E5 1B3E6A17 606AF333 3D3B4C73 E8300D06 092A8648 86F70D01 010B0500 + 03820101 00507F24 D3932A66 86025D9F E838AE5C 6D4DF6B0 49631C78 240DA905 + 604EDCDE FF4FED2B 77FC460E CD636FDB DD44681E 3A5673AB 9093D3B1 6C9E3D8B + D98987BF E40CBD9E 1AECA0C2 2189BB5C 8FA85686 CD98B646 5575B146 8DFC66A8 + 467A3DF4 4D565700 6ADF0F0D CF835015 3C04FF7C 21E878AC 11BA9CD2 55A9232C + 7CA7B7E6 C1AF74F6 152E99B7 B1FCF9BB E973DE7F 5BDDEB86 C71E3B49 1765308B + 5FB0DA06 B92AFE7F 494E8A9E 07B85737 F3A58BE1 1A48A229 C37C1E69 39F08678 + 80DDCD16 D6BACECA EEBC7CF9 8428787B 35202CDC 60E4616A B623CDBD 230E3AFB + 418616A9 4093E049 4D10AB75 27E86F73 932E35B5 8862FDAE 0275156F 719BB2F0 + D697DF7F 28 + quit + ! + ! + license udi pid C8000V sn 9EGHOCL6VW2 + memory free low-watermark processor 201711 + diagnostic bootup level minimal + ! + ! + ! + username ansible privilege 15 secret 9 $9$VslpRow9omMy..$5zvxcxrJqLeDd0qEe.5FANSAeLgz9LLAzMiAjieb/nc + ! + redundancy + ! + ! + ! + ! + ! + ! + ! + ! + interface GigabitEthernet1 + ip address dhcp + no shutdown + negotiation auto + ! + interface GigabitEthernet2 + no ip address + shutdown + negotiation auto + ! + interface GigabitEthernet3 + no ip address + shutdown + negotiation auto + ! + interface GigabitEthernet4 + no ip address + shutdown + negotiation auto + ! + ip forward-protocol nd + ! + ip http server + ip http secure-server + ip ssh bulk-mode 131072 + ! + ! + ! + ! + ! + control-plane + ! + ! + line con 0 + stopbits 1 + line aux 0 + line vty 0 4 + login local + transport input ssh + ! + ! + ! + ! + ! + ! + ! + end + cpu_limit: null + cpus: null + data_volume: null + hide_links: false + id: n2 + image_definition: null + label: iosxe + node_definition: cat8000v + parameters: {} + ram: null + tags: [] + x: -1040 + y: -80 + interfaces: + - id: i0 + label: Loopback0 + type: loopback + - id: i1 + label: GigabitEthernet1 + slot: 0 + type: physical + - id: i2 + label: GigabitEthernet2 + slot: 1 + type: physical + - id: i3 + label: GigabitEthernet3 + slot: 2 + type: physical + - id: i4 + label: GigabitEthernet4 + slot: 3 + type: physical + - boot_disk_size: null + configuration: [] + cpu_limit: null + cpus: null + data_volume: null + hide_links: false + id: n3 + image_definition: null + label: slave-switch + node_definition: unmanaged_switch + parameters: {} + ram: null + tags: [] + x: -680 + y: -240 + interfaces: + - id: i0 + label: port0 + slot: 0 + type: physical + - id: i1 + label: port1 + slot: 1 + type: physical + - id: i2 + label: port2 + slot: 2 + type: physical + - id: i3 + label: port3 + slot: 3 + type: physical + - id: i4 + label: port4 + slot: 4 + type: physical + - id: i5 + label: port5 + slot: 5 + type: physical + - id: i6 + label: port6 + slot: 6 + type: physical + - id: i7 + label: port7 + slot: 7 + type: physical + - boot_disk_size: null + configuration: [] + cpu_limit: null + cpus: null + data_volume: null + hide_links: false + id: n4 + image_definition: null + label: ci-god + node_definition: external_connector + parameters: {} + ram: null + tags: [] + x: 320 + y: -240 + interfaces: + - id: i0 + label: port + slot: 0 + type: physical + - boot_disk_size: null + configuration: [] + cpu_limit: null + cpus: null + data_volume: null + hide_links: false + id: n5 + image_definition: null + label: ubuntu + node_definition: ubuntu + parameters: {} + ram: null + tags: [] + x: -680 + y: -80 + interfaces: + - id: i0 + label: ens2 + slot: 0 + type: physical +links: + - id: l0 + n1: n3 + n2: n4 + i1: i0 + i2: i0 + conditioning: {} + label: slave-switch-port0<->ci-god-port + - id: l1 + n1: n3 + n2: n1 + i1: i1 + i2: i1 + conditioning: {} + label: slave-switch-port1<->iosxr-MgmtEth0/RP0/CPU0/0 + - id: l2 + n1: n3 + n2: n0 + i1: i2 + i2: i1 + conditioning: {} + label: slave-switch-port2<->nxos-mgmt0 + - id: l3 + n1: n3 + n2: n2 + i1: i3 + i2: i1 + conditioning: {} + label: slave-switch-port3<->iosxe-GigabitEthernet1 + - id: l4 + n1: n5 + n2: n3 + i1: i0 + i2: i4 + conditioning: {} + label: ubuntu-ens2<->slave-switch-port4 +lab: + description: Upstream lab to test ansible.netcommon + notes: Netcommon upstream test lab + title: Ansible Netcommon Test Lab + version: 0.2.2 diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/target-prefixes.network b/collections/ansible_collections/ansible/netcommon/tests/integration/target-prefixes.network new file mode 100644 index 0000000..dc64706 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/target-prefixes.network @@ -0,0 +1,4 @@ +cli +netconf +restconf +httpapi diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/fixtures/output.xml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/fixtures/output.xml new file mode 100644 index 0000000..de9b90a --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/fixtures/output.xml @@ -0,0 +1,34 @@ + + + + + + + 0/0/CPU0 + + true + ntp-leap-no-warning + + + ntp-mode-client + true +
10.1.1.1
+ 0 +
+ -1 +
+ + + ntp-mode-client + true +
172.16.252.29
+ 255 +
+ 991 +
+
+
+
+
+
+
diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/main.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/main.yaml new file mode 100644 index 0000000..c8cbcb7 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/main.yaml @@ -0,0 +1,47 @@ +--- +- name: Gather facts for non-network to get ansible_distribution + ansible.builtin.gather_facts: + when: ansible_network_os is not defined + +- name: Set a short name + ansible.builtin.set_fact: + os: "{{ ansible_distribution | d or (ansible_network_os | d('')).split('.')[-1] }}" + +- name: Run native parser tests + ansible.builtin.include_tasks: "{{ os | lower }}_native.yaml" + vars: + parser: "({{ os }}/native)" + tags: + - native + +- name: Run pyats parser tests + ansible.builtin.include_tasks: "{{ os | lower }}_pyats.yaml" + vars: + parser: "({{ os }}/pyats)" + when: "os in ['nxos']" + tags: + - pyats + +- name: Run ntc_templates parser tests + ansible.builtin.include_tasks: "{{ os | lower }}_ntc_templates.yaml" + vars: + parser: "({{ os }}/ntc_templates)" + when: "os in ['nxos']" + tags: + - ntc_templates + +- name: Run textfsm parser tests + ansible.builtin.include_tasks: "{{ os | lower }}_textfsm.yaml" + vars: + parser: "({{ os }}/textfsm)" + when: "os in ['nxos']" + tags: + - textfsm + +- name: Run XML parser tests + ansible.builtin.include_tasks: "{{ os | lower }}_xml.yaml" + vars: + parser: "({{ os }}/xml)" + when: "os in ['nxos']" + tags: + - xml diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_native.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_native.yaml new file mode 100644 index 0000000..74344ab --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_native.yaml @@ -0,0 +1,56 @@ +--- +- name: "Run command and parse with {{ parser }}" + ansible.utils.cli_parse: + command: "show interface" + parser: + name: ansible.netcommon.native + set_fact: POpqMQoJWTiDpEW + register: nxos_native_command + +- name: "Pass text and template_path to {{ parser }}" + ansible.utils.cli_parse: + text: "{{ nxos_native_command['stdout'] }}" + parser: + name: ansible.netcommon.native + template_path: "{{ role_path }}/templates/nxos_show_interface.yaml" + register: nxos_native_text + +- name: "Check output" + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ POpqMQoJWTiDpEW is defined }}" + - "{{ nxos_native_command['parsed']['mgmt0'] is defined }}" + - "{{ nxos_native_command['stdout'] is defined }}" + - "{{ nxos_native_command['stdout_lines'] is defined }}" + - "{{ nxos_native_command['parsed'] == nxos_native_text['parsed'] }}" + +- name: "Pass text and command to {{ parser }}" + ansible.utils.cli_parse: + text: "{{ nxos_native_command['stdout'] }}" + parser: + name: ansible.netcommon.native + command: show interface + register: output + +- name: "Check output" + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ output['parsed']['mgmt0'] is defined }}" + +- name: "Run command and parse with {{ parser }}" + ansible.utils.cli_parse: + command: "sho int" + parser: + name: ansible.netcommon.native + command: show interface + register: output + +- name: "Check output" + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ output['parsed']['mgmt0'] is defined }}" + - "{{ output['stdout'] is defined }}" + - "{{ output['stdout_lines'] is defined }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_ntc_templates.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_ntc_templates.yaml new file mode 100644 index 0000000..9e33ad1 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_ntc_templates.yaml @@ -0,0 +1,24 @@ +--- +- name: "Run command and parse with {{ parser }}" + ansible.utils.cli_parse: + command: "show interface" + parser: + name: ansible.netcommon.ntc_templates + register: nxos_ntc_templates_command + +- name: "Pass text and command to {{ parser }}" + ansible.utils.cli_parse: + text: "{{ nxos_ntc_templates_command['stdout'] }}" + parser: + name: ansible.netcommon.ntc_templates + command: show interface + register: nxos_ntc_templates_text + +- name: "Check output" + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ nxos_ntc_templates_command['stdout'] is defined }}" + - "{{ nxos_ntc_templates_command['stdout_lines'] is defined }}" + - "{{ nxos_ntc_templates_command['parsed'] == nxos_ntc_templates_text['parsed'] }}" + - "{{ (nxos_ntc_templates_command['parsed'] | selectattr('interface', 'search', 'mgmt0') | list | length) > 0 }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_pyats.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_pyats.yaml new file mode 100644 index 0000000..28c9e44 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_pyats.yaml @@ -0,0 +1,73 @@ +--- +- name: Determine the python version of the control node + ansible.builtin.command: "python --version" + delegate_to: localhost + register: pyver + changed_when: false + +- name: Python 2 reports to stderr, Python 3 to stdout + ansible.builtin.set_fact: + pyver_found: "{{ ((pyver['stderr'] or pyver['stdout']).split())[1][0] }}" + +- name: Ensure a valid python major version was found + ansible.builtin.assert: + that: "{{ pyver_found in ['2', '3'] }}" + +- name: Only run pyats tests when control node is Python3 + when: pyver_found == '3' + block: + - name: "Run command and parse with {{ parser }}" + ansible.utils.cli_parse: + command: "show interface" + parser: + name: ansible.netcommon.pyats + register: nxos_pyats_command + + - name: "Pass text and command to {{ parser }}" + ansible.utils.cli_parse: + text: "{{ nxos_pyats_command['stdout'] }}" + parser: + name: ansible.netcommon.pyats + command: show interface + register: nxos_pyats_text + + - name: "Check output" + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ nxos_pyats_command['stdout'] is defined }}" + - "{{ nxos_pyats_command['stdout_lines'] is defined }}" + - "{{ nxos_pyats_command['parsed'] == nxos_pyats_text['parsed'] }}" + - "{{ nxos_pyats_command['parsed']['mgmt0'] is defined }}" + + - name: "Pass text, command, and os" + ansible.utils.cli_parse: + text: "{{ nxos_pyats_command['stdout'] }}" + parser: + name: ansible.netcommon.pyats + command: show interface + os: nxos + register: nxos_pyats_text + + - name: "Check output" + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ nxos_pyats_command['stdout'] is defined }}" + - "{{ nxos_pyats_command['stdout_lines'] is defined }}" + - "{{ nxos_pyats_command['parsed'] == nxos_pyats_text['parsed'] }}" + - "{{ nxos_pyats_command['parsed']['mgmt0'] is defined }}" + + - name: "Confirm parser found message" + ansible.utils.cli_parse: + text: "{{ nxos_pyats_command['stdout'] }}" + parser: + name: ansible.netcommon.pyats + command: invalid_parser_name + os: nxos + register: nxos_pyats_invalid_parser_name + ignore_errors: true + + - name: "Check output" + ansible.builtin.assert: + that: "{{ 'Could not find parser' in nxos_pyats_invalid_parser_name['msg'] }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_textfsm.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_textfsm.yaml new file mode 100644 index 0000000..9223750 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_textfsm.yaml @@ -0,0 +1,22 @@ +--- +- name: "Fetch command output" + cisco.nxos.nxos_command: + commands: + - show version + register: version_output + +- name: Set path for textfsm templates + ansible.builtin.set_fact: + textfsm_template_path: "{{ role_path }}/templates/nxos_show_version.textfsm" + +- name: "Invoke parse_cli_textfsm" + ansible.builtin.set_fact: + device_neighbors: "{{ version_output.stdout[0] | parse_cli_textfsm(textfsm_template_path) }}" + register: nxos_textfsm_text + +- name: Check textfsm parse output (would fail on underlying device update) + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ nxos_textfsm_text['ansible_facts']['device_neighbors'][0]['BOOT_IMAGE'] == 'bootflash:///nxos64-cs.10.4.2.F.bin' }}" + - "{{ nxos_textfsm_text['ansible_facts']['device_neighbors'][0]['PLATFORM'] == 'C9300v' }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_xml.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_xml.yaml new file mode 100644 index 0000000..8002583 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/nxos_xml.yaml @@ -0,0 +1,21 @@ +--- +- name: Set path for xml templates and fixture + ansible.builtin.set_fact: + xml_template_path: "{{ role_path }}/templates/nxos_parse_xml.yml" + xml_fixture_path: "{{ role_path }}/fixtures/output.xml" + +- name: Facts setup + ansible.builtin.set_fact: + xml: "{{ lookup('file', xml_fixture_path) }}" + +- name: Parse xml invocation + ansible.builtin.debug: + msg: "{{ xml | ansible.netcommon.parse_xml(xml_template_path) }}" + register: nxos_xml_text + +- name: Check xml parse output + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ nxos_xml_text['msg']['result'][0]['address'][0] == '10.1.1.1' }}" + - "{{ nxos_xml_text['msg']['result'][0]['reachability'][0] == '0' }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/ubuntu_native.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/ubuntu_native.yaml new file mode 100644 index 0000000..77e42ca --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/tasks/ubuntu_native.yaml @@ -0,0 +1,31 @@ +--- +- name: "Run command and parse with {{ parser }}" + ansible.utils.cli_parse: + command: "ps -ef" + parser: + name: ansible.netcommon.native + register: ps_out + +- name: "Check output" + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ ps_out['stdout'] is defined }}" + - "{{ ps_out['stdout_lines'] is defined }}" + - "{{ ps_out['parsed'] is defined }}" + +- name: "Run command and parse with {{ parser }}" + ansible.utils.cli_parse: + command: "iptables --list" + parser: + name: ansible.netcommon.native + become: true + register: ipt_out + +- name: "Check output" + ansible.builtin.assert: + that: "{{ item }}" + with_items: + - "{{ ipt_out['stdout'] is defined }}" + - "{{ ipt_out['stdout_lines'] is defined }}" + - "{{ ipt_out['parsed'] is defined }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_parse_xml.yml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_parse_xml.yml new file mode 100644 index 0000000..50a6eed --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_parse_xml.yml @@ -0,0 +1,12 @@ +--- +vars: + ntp_peers: + address: "{{ item.address }}" + reachability: "{{ item.reachability}}" +keys: + result: + value: "{{ ntp_peers }}" + top: data/ntp/nodes/node/associations + items: + address: peer-summary-info/peer-info-common/address + reachability: peer-summary-info/peer-info-common/reachability diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_interface.ttp b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_interface.ttp new file mode 100644 index 0000000..7ec545d --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_interface.ttp @@ -0,0 +1,3 @@ +{{ interface }} is {{ state }} +admin state is {{ admin_state }}{{ ignore(".*") }} +{{ var | set("extra_var") }} diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_interface.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_interface.yaml new file mode 100644 index 0000000..fa3ad79 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_interface.yaml @@ -0,0 +1,24 @@ +--- +- example: Ethernet1/1 is up + getval: '(?P\S+) is (?P\S+)' + result: + "{{ name }}": + name: "{{ name }}" + state: + operating: "{{ oper_state }}" + shared: true + +- example: admin state is up, Dedicated Interface + getval: 'admin state is (?P\S+)' + result: + "{{ name }}": + name: "{{ name }}" + state: + admin: "{{ admin_state }}" + +- example: " Hardware: Ethernet, address: 5254.005a.f8b5 (bia 5254.005a.f8b5)" + getval: '\s+Hardware: (?P.*), address: (?P\S+)' + result: + "{{ name }}": + hardware: "{{ hardware }}" + mac_address: "{{ mac }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_version.textfsm b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_version.textfsm new file mode 100644 index 0000000..06fb92b --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/nxos_show_version.textfsm @@ -0,0 +1,16 @@ +Value UPTIME ((\d+\s\w+.s.,?\s?){4}) +Value LAST_REBOOT_REASON (.+) +Value OS (\d+.\d+(.+)?) +Value BOOT_IMAGE (.*) +Value PLATFORM (\w+) + +Start + ^\s+(NXOS: version|system:\s+version)\s+${OS}\s*$$ + ^\s+(NXOS|kickstart)\s+image\s+file\s+is:\s+${BOOT_IMAGE}\s*$$ + ^\s+cisco\s+${PLATFORM}\s+[cC]hassis + ^\s+cisco\s+Nexus\d+\s+${PLATFORM} + # Cisco N5K platform + ^\s+cisco\s+Nexus\s+${PLATFORM}\s+[cC]hassis + ^\s+cisco\s+.+-${PLATFORM}\s* + ^Kernel\s+uptime\s+is\s+${UPTIME} + ^\s+Reason:\s${LAST_REBOOT_REASON} -> Record diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/ubuntu_iptables_--list.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/ubuntu_iptables_--list.yaml new file mode 100644 index 0000000..4d2e55f --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/ubuntu_iptables_--list.yaml @@ -0,0 +1,41 @@ +--- +- example: "Chain FORWARD (policy ACCEPT)" + getval: 'Chain\s(?P\S+)\s+\((policy\s(?P\S+))?((?P\d+)\sreferences)?\)$' + result: + "{{ chain_name }}": + name: "{{ chain_name }}" + policy: "{{ policy }}" + references: "{{ references }}" + shared: true + +- example: "target prot opt source destination" + getval: "^target.*" + result: {} + +- example: "ACCEPT all -- anywhere 192.168.122.0/24 ctstate RELATED,ESTABLISHED" + getval: | + (?x) + (?P\S+) + \s+(?P\S+) + \s+(?P\S+) + \s+(?P\S+) + \s+(?P\S+) + (\s+ctstate\s(?P\S+))? + (\s+reject-with\s(?P\S+))? + (\s+udp\sdpt:(?P\S+))? + (\s+tcp\sdpt:(?P\S+))? + + result: + "{{ chain_name }}": + rules: + target: "{{ target }}" + protocol: "{{ protocol }}" + opt: "{% if opt == '--' %}None{% else %}{{ opt }}{% endif %}" + source: "{{ source }}" + destination: "{{ destination }}" + connection_tracking: + states: "{{ ctstates.split(',') }}" + reject_with: "{{ reject_with }}" + destination_port: + udp: "{{ udp_dpt }}" + tcp: "{{ tcp_dpt }}" diff --git a/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/ubuntu_ps_-ef.yaml b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/ubuntu_ps_-ef.yaml new file mode 100644 index 0000000..ceca8b9 --- /dev/null +++ b/collections/ansible_collections/ansible/netcommon/tests/integration/targets/cli_parse/templates/ubuntu_ps_-ef.yaml @@ -0,0 +1,17 @@ +--- +- example: "UID PID PPID C STIME TTY TIME CMD" + getval: "^UID.*" + result: {} + +- example: "root 2 0 0 Jul28 ? 00:00:00 [kthreadd]" + getval: '(?P\S+)\s+(?P\S+)\s+(?P\S+)\s+(?P\S+)\s+(?P\S+)\s+(?P\S+)\s+(?P