diff --git a/.ansible-lint b/.ansible-lint index cdf866c..35bd547 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -14,6 +14,8 @@ exclude_paths: - .dev_dir/example_dev_vars.yml - .dev_dir/* - playbooks/tests/configuration_file_dev_vars.yml + # External SDK imports are explicitly baselined for ansible-test 2.21. + - tests/sanity/ignore-2.21.txt - .ansible # parseable: true # quiet: true diff --git a/.dev_dir/software_upgrades_with_remote_server.yml b/.dev_dir/software_upgrades_with_remote_server.yml index e414a1a..cc9c18e 100644 --- a/.dev_dir/software_upgrades_with_remote_server.yml +++ b/.dev_dir/software_upgrades_with_remote_server.yml @@ -4,7 +4,7 @@ --- # This playbook uses example role for remote upgrades. -# It assumes that controllers are <20.13 version and Edge are <17.13 version. +# Run the Manager release compatibility smoke test before destructive upgrade testing. # It configures Remote Server, uploads images, then perform installation, activation and default version set. # Doesn't include cEdge software upgrades - known bug with cEdge with Remote Images # Vars provided in this playbook are dev_vars diff --git a/.flake8 b/.flake8 index 2a7120a..b7982af 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,2 @@ [flake8] -ignore = E402 +ignore = E402,E704 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f08b2fe --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.j2 text eol=lf +*.py text eol=lf +*.yml text eol=lf +*.yaml text eol=lf diff --git a/.github/workflows/ansible-doc-validation.yml b/.github/workflows/ansible-doc-validation.yml index abaafc1..87fa3df 100644 --- a/.github/workflows/ansible-doc-validation.yml +++ b/.github/workflows/ansible-doc-validation.yml @@ -18,12 +18,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.10' + python-version: '3.13' - name: Get collection namespace and name from galaxy.yml id: collection-name @@ -40,7 +40,7 @@ jobs: echo "COLLECTION_NAME=$NAME" >> $GITHUB_ENV - name: Install Ansible - run: pip install ansible==9.4.0 + run: pip install ansible==14.3.1 catalystwan==0.41.5.dev2 - name: Build and install the collection run: | diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..1d4fa65 --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,51 @@ +name: Compatibility + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: read-all + +jobs: + sdk-and-ansible: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14"] + catalystwan-version: ["0.41.5.dev2", "0.41.6"] + + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "${{ matrix.python-version }}" + + - name: Install the current automation stack + run: | + python -m pip install --upgrade pip + python -m pip install \ + "ansible==14.3.1" \ + "catalystwan==${{ matrix.catalystwan-version }}" \ + "pytest==9.1.1" + + - name: Run SDK compatibility tests + run: python -m pytest -q tests/unit + + - name: Build and install the collection + run: | + ansible-galaxy collection build . --force + ansible-galaxy collection install cisco-catalystwan-*.tar.gz --force + + - name: Validate module documentation imports + run: | + for module in plugins/modules/*.py; do + module_name="${module##*/}" + ansible-doc --type module "cisco.catalystwan.${module_name%.py}" >/dev/null + done diff --git a/.github/workflows/galaxy-importer.yml b/.github/workflows/galaxy-importer.yml index 47ba7b4..9052a70 100644 --- a/.github/workflows/galaxy-importer.yml +++ b/.github/workflows/galaxy-importer.yml @@ -21,12 +21,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.10' + python-version: '3.13' - name: Get collection namespace and name from galaxy.yml id: collection-name @@ -47,7 +47,7 @@ jobs: - name: Install Ansible and importer run: | - pip install ansible==9.4.0 + pip install ansible==14.3.1 pip install ansible-importer pip install galaxy-importer diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 600bb92..38850ca 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -5,7 +5,7 @@ on: push: branches: - - master + - main permissions: read-all @@ -14,27 +14,27 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10'] + python-version: ['3.14'] defaults: run: shell: sh steps: - name: Check out the repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.10' + python-version: '3.14' - uses: isort/isort-action@master with: requirementsFiles: "requirements.txt" - isortVersion: '5.12.0' + isortVersion: '8.0.1' - name: Install pycln - run: pip install pycln==2.4.0 + run: pip install pycln==2.6.0 - name: Run pycln run: pycln --all . @@ -43,10 +43,10 @@ jobs: uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - name: Run ansible-lint - uses: ansible/ansible-lint@v25.8.1 + uses: ansible/ansible-lint@665d9e07a1943254d2910faffc106adaf7ea7294 # v26.8.0 - name: Install detect-secrets - run: pip install detect-secrets==1.4.0 + run: pip install detect-secrets==1.5.0 - name: Run detect-secrets run: detect-secrets scan diff --git a/.github/workflows/release-from-tag.yml b/.github/workflows/release-from-tag.yml index 7155864..6ec4aca 100644 --- a/.github/workflows/release-from-tag.yml +++ b/.github/workflows/release-from-tag.yml @@ -14,12 +14,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.10' + python-version: '3.13' - name: Get collection version from galaxy.yml id: collection-version @@ -57,7 +57,7 @@ jobs: - name: Install Ansible run: | - pip install ansible==9.4.0 + pip install ansible==14.3.1 - name: Build collection run: | diff --git a/.github/workflows/sanity.yml b/.github/workflows/sanity.yml new file mode 100644 index 0000000..ca4c544 --- /dev/null +++ b/.github/workflows/sanity.yml @@ -0,0 +1,38 @@ +name: Ansible sanity + +on: + pull_request: + push: + branches: + - main + schedule: + - cron: "23 9 * * 1" + workflow_dispatch: + +permissions: read-all + +jobs: + sanity: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14"] + + steps: + - name: Check out the collection + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: ansible_collections/cisco/catalystwan + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "${{ matrix.python-version }}" + + - name: Install ansible-test + run: python -m pip install "ansible==14.3.1" + + - name: Run collection sanity tests + working-directory: ansible_collections/cisco/catalystwan + run: ansible-test sanity --python "${{ matrix.python-version }}" --color yes diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f62af6..2c2b1ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,47 +1,47 @@ files: '.py' exclude: '.git' -default_stages: [commit] +default_stages: [pre-commit] repos: - - repo: https://github.com/timothycrosley/isort - rev: 5.12.0 + - repo: https://github.com/PyCQA/isort + rev: 8.0.1 hooks: - id: isort args: ["-l", "120", "--profile", "black"] - repo: https://github.com/hadialqattan/pycln - rev: v2.5.0 + rev: v2.6.0 hooks: - id: pycln args: ["--all"] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 26.5.1 hooks: - id: black args: ["-l", "120"] - repo: https://github.com/pycqa/flake8 - rev: 6.1.0 + rev: 7.3.0 hooks: - id: flake8 args: ["--max-line-length", "120"] fail_fast: true - repo: https://github.com/ansible-community/ansible-lint.git - rev: v25.8.1 + rev: v26.8.0 hooks: - id: ansible-lint files: \.(yaml|yml)$ - repo: https://github.com/Yelp/detect-secrets - rev: v1.4.0 + rev: v1.5.0 hooks: - id: detect-secrets diff --git a/README.md b/README.md index c097368..8aa34c4 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,14 @@ Support for the following workflows in vManage client and as Ansible modules: Currently development of the tool was set with: -- Python = 3.10.0 -- Ansible = 2.16.6 -- catalystwan = "^0.33.6post0" +- Python = 3.12-3.14 +- Ansible = 14.3.1 (ansible-core 2.21.3) +- catalystwan = 0.41.5.dev2 for the complete high-level API used by the collection + +The compatibility CI also imports and validates module documentation with the +latest stable `catalystwan` SDK (0.41.6). That stable SDK removed several +high-level APIs; affected modules return a focused dependency error instead of +making a partial or unsafe change. See [the compatibility policy](docs/COMPATIBILITY.md). ## Installing this collection diff --git a/ansible.cfg b/ansible.cfg index 52e85f2..c5762af 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,7 +1,8 @@ [defaults] roles_path = ./roles #vault_password_file = /path/to/vault/password/file -stdout_callback = yaml +stdout_callback = default +callback_result_format = yaml bin_ansible_callbacks = True callbacks_enabled = profile_tasks remote_tmp = /tmp/.ansible_remote/tmp diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md new file mode 100644 index 0000000..591c248 --- /dev/null +++ b/docs/COMPATIBILITY.md @@ -0,0 +1,42 @@ +# Compatibility policy + +## Current targets + +| Layer | Tested target | Validation | +| --- | --- | --- | +| Python | 3.12, 3.13, 3.14 | CI matrix | +| Ansible | 14.3.1 / ansible-core 2.21.3 | Unit, collection build, and `ansible-doc` checks | +| catalystwan SDK | 0.41.5.dev2 | Full module import and SDK contract checks | +| catalystwan SDK | 0.41.6 | Full module import and documentation checks; removed SDK APIs fail with actionable messages | +| Cisco Catalyst SD-WAN Manager | 26.1 | SDK version parsing plus opt-in live smoke playbook | + +The collection retains `requires_ansible: >=2.16.6` for existing users, while +the newest automation stack is tested on every change. + +## Live Manager validation + +Static SDK checks cannot prove that a Manager endpoint behaves identically in a +new software release. Before declaring a new Manager train fully validated, run: + +```bash +ansible-playbook playbooks/tests/test_manager_release_compatibility.yml \ + -e @manager_credentials.yml \ + -e target_manager_release=26.1 +``` + +`manager_credentials.yml` must define `manager_authentication` with `url`, +`username`, `password`, and the HTTPS API `port` when it is not 443. The +playbook reads the installed release and checks API readiness without changing +Manager state. If the shell also uses `VMANAGE_PORT` for SSH, pass the HTTPS +port explicitly in `manager_authentication` so the SDK does not inherit the SSH +port. + +## SDK compatibility note + +The stable catalystwan 0.41.x package removed high-level implementations used +by `cluster_management`, `config_groups`, `device_templates_recovery`, and the +Enterprise Root CA option of `administration_settings`. The collection remains +fully functional with the latest published legacy API artifact, +`catalystwan==0.41.5.dev2`, while also keeping all modules importable with +0.41.6. Operations whose upstream API is absent stop before making a request and +explain which SDK artifact is required. diff --git a/galaxy.yml b/galaxy.yml index aa8177a..7261bca 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: cisco name: catalystwan -version: 0.3.3 +version: 0.3.4 readme: README.md authors: - Arkadiusz Cichon @@ -22,4 +22,11 @@ tags: - networking dependencies: {} -build_ignore: [] +build_ignore: + - .git + - .github + - .pytest_cache + - .venv + - "**/__pycache__" + - "**/*.pyc" + - "*.tar.gz" diff --git a/playbooks/tests/data/config_groups.yml b/playbooks/tests/data/config_groups.yml index 35f28a0..e4d0641 100644 --- a/playbooks/tests/data/config_groups.yml +++ b/playbooks/tests/data/config_groups.yml @@ -1,1129 +1,1129 @@ -### SYSTEM PARCELS - -banner_parcel: - type: banner - config: - name: Banner - description: Banner Description - data: - login: - optionType: default - value: "" - motd: - optionType: default - value: "" - -basic_parcel: - type: basic - config: - name: Basic - description: Basic Setting Description - data: - affinityGroupNumber: - optionType: default - affinityGroupPreference: - optionType: default - affinityPerVrf: - - affinityGroupNumber: - optionType: default - vrfRange: - optionType: default - affinityPreferenceAuto: - optionType: default - value: false - siteType: - optionType: default - clock: - timezone: - optionType: default - value: UTC - description: - optionType: default - location: - optionType: default - gpsLocation: - latitude: - optionType: default - longitude: - optionType: default - deviceGroups: - optionType: default - controllerGroupList: - optionType: default - overlayId: - optionType: default - value: 1 - portOffset: - optionType: default - value: 0 - portHop: - optionType: default - value: true - controlSessionPps: - optionType: default - value: 300 - trackTransport: - optionType: default - value: true - trackInterfaceTag: - optionType: default - consoleBaudRate: - optionType: default - value: '9600' - maxOmpSessions: - optionType: default - multiTenant: - optionType: default - value: false - trackDefaultGateway: - optionType: default - value: true - adminTechOnFailure: - optionType: default - value: true - idleTimeout: - optionType: default - onDemand: - onDemandEnable: - optionType: default - value: false - onDemandIdleTimeout: - optionType: default - value: 10 - -bfd_parcel: - type: bfd - config: - name: BFD - description: BFD Description - data: - pollInterval: - optionType: default - value: 600000 - multiplier: - optionType: default - value: 6 - defaultDscp: - optionType: default - value: 48 - -omp_parcel: - type: omp - config: - name: OMP - description: OMP Description - data: - gracefulRestart: - optionType: default - value: true - sendPathLimit: - optionType: default - value: 4 - ecmpLimit: - optionType: default - value: 4 - advertisementInterval: - optionType: default - value: 1 - holdtime: - optionType: default - value: 60 - eorTimer: - optionType: default - value: 300 - overlayAs: - optionType: default - shutdown: - optionType: default - value: false - gracefulRestartTimer: - optionType: default - value: 43200 - ompAdminDistanceIpv4: - optionType: default - value: 251 - ompAdminDistanceIpv6: - optionType: default - value: 251 - advertiseIpv4: - bgp: - optionType: default - value: false - ospf: - optionType: default - value: false - ospfv3: - optionType: default - value: false - connected: - optionType: default - value: true - static: - optionType: default - value: true - eigrp: - optionType: default - value: false - lisp: - optionType: default - value: false - isis: - optionType: default - value: false - advertiseIpv6: - bgp: - optionType: default - value: false - ospf: - optionType: default - value: false - static: - optionType: default - value: false - eigrp: - optionType: default - value: false - connected: - optionType: default - value: false - lisp: - optionType: default - value: false - isis: - optionType: default - value: false - ignoreRegionPathLength: - optionType: default - value: false - transportGateway: - optionType: default - siteTypesForTransportGateway: - optionType: default - -logging_parcel: - type: logging - config: - name: Logging - description: Logging Description - data: - disk: - file: - diskFileSize: - optionType: default - value: 10 - diskFileRotate: - optionType: default - value: 10 - -ntp_parcel: - type: ntp - config: - name: NTP - description: NTP Description - data: - server: - - name: - optionType: global - value: time.google.com - vpn: - optionType: default - value: 0 - version: - optionType: default - value: 4 - key: - optionType: default - sourceInterface: - optionType: default - prefer: - optionType: default - value: false - -global_parcel: - type: global - config: - name: Global - description: Global Description - data: - services_global: - services_ip: - servicesGlobalServicesIpHttpServer: - optionType: default - value: false - servicesGlobalServicesIpHttpsServer: - optionType: default - value: false - servicesGlobalServicesIpFtpPassive: - optionType: default - value: false - servicesGlobalServicesIpDomainLookup: - optionType: default - value: false - servicesGlobalServicesIpArpProxy: - optionType: default - value: false - servicesGlobalServicesIpRcmd: - optionType: default - value: false - servicesGlobalServicesIpLineVty: - optionType: default - value: false - servicesGlobalServicesIpCdp: - optionType: default - value: true - servicesGlobalServicesIpLldp: - optionType: default - value: true - servicesGlobalServicesIpSourceIntrf: - optionType: default - globalOtherSettingsTcpKeepalivesIn: - optionType: default - value: true - globalOtherSettingsTcpKeepalivesOut: - optionType: default - value: true - globalOtherSettingsTcpSmallServers: - optionType: default - value: false - globalOtherSettingsUdpSmallServers: - optionType: default - value: false - globalOtherSettingsConsoleLogging: - optionType: default - value: true - globalOtherSettingsIPSourceRoute: - optionType: default - value: false - globalOtherSettingsVtyLineLogging: - optionType: default - value: false - globalOtherSettingsSnmpIfindexPersist: - optionType: default - value: true - globalOtherSettingsIgnoreBootp: - optionType: default - value: true - globalSettingsNat64UdpTimeout: - optionType: default - value: 300 - globalSettingsNat64TcpTimeout: - optionType: default - value: 3600 - globalSettingsHttpAuthentication: - optionType: default - globalSettingsSSHVersion: - optionType: default - -aaa_parcel: - type: aaa - config: - name: AAA - description: AAA Profile Feature Description - data: - authenticationGroup: - optionType: default - value: false - accountingGroup: - optionType: default - value: false - serverAuthOrder: - optionType: global - value: - - local - authorizationConsole: - optionType: default - value: false - authorizationConfigCommands: - optionType: default - value: false - user: - - name: - optionType: global - value: admin - password: - optionType: variable - value: "{{ '{{admin_password}}' }}" - privilege: - optionType: default - value: '15' - -mrf_parcel: - type: mrf - config: - name: MRF - description: Multi Region Fabric Description - data: - role: - optionType: global - value: edge-router - -### TRANSPORT PARCELS - -wan_interface_ethernet_parcel_1: - type: ethernet - config: - data: - multiRegionFabric: - coreRegion: - optionType: default - value: core-shared - enableCoreRegion: - optionType: default - value: false - enableSecondaryRegion: - optionType: default - value: false - secondaryRegion: - optionType: default - value: secondary-shared - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{vpn_0_mpls_if}}' }}" - description: - optionType: global - value: WAN VPN 0 Interface - MPLS - blockNonSourceIp: - optionType: default - value: false - autoDetectBandwidth: - optionType: default - value: false - tunnelInterface: - optionType: global - value: true - tunnel: - perTunnelQos: - optionType: default - value: false - color: - optionType: global - value: mpls - restrict: - optionType: default - value: false - group: - optionType: default - border: - optionType: default - value: false - maxControlConnections: - optionType: default - vBondAsStunServer: - optionType: default - value: false - excludeControllerGroupList: - optionType: default - vManageConnectionPreference: - optionType: default - value: 5 - portHop: - optionType: default - value: true - lowBandwidthLink: - optionType: default - value: false - tunnelTcpMss: - optionType: default - clearDontFragment: - optionType: default - value: false - ctsSgtPropagation: - optionType: default - value: false - networkBroadcast: - optionType: default - value: false - allowService: - all: - optionType: default - value: false - bgp: - optionType: default - value: false - dhcp: - optionType: default - value: true - ntp: - optionType: default - value: true - ssh: - optionType: default - value: true - dns: - optionType: default - value: true - icmp: - optionType: default - value: true - https: - optionType: default - value: true - ospf: - optionType: default - value: false - stun: - optionType: default - value: false - snmp: - optionType: default - value: false - netconf: - optionType: default - value: false - bfd: - optionType: default - value: false - nat: - optionType: default - value: false - intfIpAddress: - dynamic: - dynamicDhcpDistance: - optionType: default - value: 1 - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default - value: true - encapsulation: - - encap: - optionType: global - value: ipsec - weight: - optionType: default - value: 1 - preference: - optionType: default - name: MPLS - description: WAN VPN 0 MPLS Feature - -wan_interface_ethernet_parcel_2: - type: ethernet - config: - data: - multiRegionFabric: - coreRegion: - optionType: default - value: core-shared - enableCoreRegion: - optionType: default - value: false - enableSecondaryRegion: - optionType: default - value: false - secondaryRegion: - optionType: default - value: secondary-shared - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{vpn_0_public-internet_if}}' }}" - description: - optionType: global - value: WAN VPN 0 Interface - Internet - blockNonSourceIp: - optionType: default - value: false - autoDetectBandwidth: - optionType: default - value: false - tunnelInterface: - optionType: global - value: true - tunnel: - perTunnelQos: - optionType: default - value: false - color: - optionType: global - value: public-internet - restrict: - optionType: default - value: false - group: - optionType: default - border: - optionType: default - value: false - maxControlConnections: - optionType: default - vBondAsStunServer: - optionType: default - value: false - excludeControllerGroupList: - optionType: default - vManageConnectionPreference: - optionType: default - value: 5 - portHop: - optionType: default - value: true - lowBandwidthLink: - optionType: default - value: false - tunnelTcpMss: - optionType: default - clearDontFragment: - optionType: default - value: false - ctsSgtPropagation: - optionType: default - value: false - networkBroadcast: - optionType: default - value: false - allowService: - all: - optionType: default - value: false - bgp: - optionType: default - value: false - dhcp: - optionType: default - value: true - ntp: - optionType: default - value: true - ssh: - optionType: default - value: true - dns: - optionType: default - value: true - icmp: - optionType: default - value: true - https: - optionType: default - value: true - ospf: - optionType: default - value: false - stun: - optionType: default - value: false - snmp: - optionType: default - value: false - netconf: - optionType: default - value: false - bfd: - optionType: default - value: false - nat: - optionType: default - value: false - intfIpAddress: - dynamic: - dynamicDhcpDistance: - optionType: default - value: 1 - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default - value: true - encapsulation: - - encap: - optionType: global - value: ipsec - weight: - optionType: default - value: 1 - preference: - optionType: default - name: Internet - description: WAN VPN 0 Internet Feature - -wan_interface_ethernet_parcel_3: - type: ethernet - config: - data: - multiRegionFabric: - coreRegion: - optionType: default - value: core-shared - enableCoreRegion: - optionType: default - value: false - enableSecondaryRegion: - optionType: default - value: false - secondaryRegion: - optionType: default - value: secondary-shared - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{vpn_0_lte_if}}' }}" - description: - optionType: global - value: WAN VPN 0 Interface - LTE - blockNonSourceIp: - optionType: default - value: false - autoDetectBandwidth: - optionType: default - value: false - tunnelInterface: - optionType: global - value: true - tunnel: - perTunnelQos: - optionType: default - value: false - color: - optionType: global - value: lte - restrict: - optionType: default - value: false - group: - optionType: default - border: - optionType: default - value: false - maxControlConnections: - optionType: default - vBondAsStunServer: - optionType: default - value: false - excludeControllerGroupList: - optionType: default - vManageConnectionPreference: - optionType: default - value: 5 - portHop: - optionType: default - value: true - lowBandwidthLink: - optionType: default - value: false - tunnelTcpMss: - optionType: default - clearDontFragment: - optionType: default - value: false - ctsSgtPropagation: - optionType: default - value: false - networkBroadcast: - optionType: default - value: false - allowService: - all: - optionType: default - value: false - bgp: - optionType: default - value: false - dhcp: - optionType: default - value: true - ntp: - optionType: default - value: true - ssh: - optionType: default - value: true - dns: - optionType: default - value: true - icmp: - optionType: default - value: true - https: - optionType: default - value: true - ospf: - optionType: default - value: false - stun: - optionType: default - value: false - snmp: - optionType: default - value: false - netconf: - optionType: default - value: false - bfd: - optionType: default - value: false - nat: - optionType: default - value: false - intfIpAddress: - dynamic: - dynamicDhcpDistance: - optionType: default - value: 1 - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default - value: true - encapsulation: - - encap: - optionType: global - value: ipsec - weight: - optionType: default - value: 1 - preference: - optionType: default - name: LTE - description: WAN VPN 0 LTE Feature - -wan_vpn_parcel: - type: vpn - config: - name: VPN0 - description: SDWAN Transport Wan Vpn Feature config - data: - vpnId: - optionType: default - value: 0 - enhanceEcmpKeying: - optionType: global - value: true - ipv4Route: [] - sub_parcels: - - "{{ wan_interface_ethernet_parcel_1 }}" - - "{{ wan_interface_ethernet_parcel_2 }}" - - "{{ wan_interface_ethernet_parcel_3 }}" - -### SERVICE PARCELS - -lan_interface_ethernet_parcel_1: - type: ethernet - config: - name: VPN_Payment_Processing_Network_12_Interface - description: LAN Interface - data: - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{vpn_12_if_0}}' }}" - description: - optionType: default - intfIpAddress: - static: - staticIpV4AddressPrimary: - ipAddress: - optionType: variable - value: "{{ '{{vpn_12_if_0_static_ipaddr}}' }}" - subnetMask: - optionType: variable - value: "{{ '{{vpn_12_if_0_static_subnet}}' }}" - nat: - optionType: default - value: false - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default - value: true - -lan_vpn_parcel_1: - type: vpn - config: - name: Payment_Processing_Network - description: LAN VPN - data: - vpnId: - optionType: global - value: 12 - name: - optionType: global - value: Payment Processing Network - ipv4Route: [] - enableSdra: - optionType: global - value: false - sub_parcels: - - "{{ lan_interface_ethernet_parcel_1 }}" - -lan_interface_ethernet_parcel_2: - type: ethernet - config: - name: VPN_Physical_Security_Devices_13_Interface - description: LAN Interface - data: - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{vpn_13_if_0}}' }}" - description: - optionType: default - intfIpAddress: - static: - staticIpV4AddressPrimary: - ipAddress: - optionType: variable - value: "{{ '{{vpn_13_if_0_static_ipaddr}}' }}" - subnetMask: - optionType: variable - value: "{{ '{{vpn_13_if_0_static_subnet}}' }}" - nat: - optionType: default - value: false - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default - value: true - -lan_vpn_parcel_2: - type: vpn - config: - name: Physical_Security_Devices - description: LAN VPN - data: - vpnId: - optionType: global - value: 13 - name: - optionType: global - value: Physical Security Devices - ipv4Route: [] - enableSdra: - optionType: global - value: false - sub_parcels: - - "{{ lan_interface_ethernet_parcel_2 }}" - -lan_interface_ethernet_parcel_3: - type: ethernet - config: - name: VPN_Local_Internet_for_Guests_99_Interface - description: LAN Interface - data: - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{vpn_99_if_0}}' }}" - description: - optionType: default - intfIpAddress: - static: - staticIpV4AddressPrimary: - ipAddress: - optionType: variable - value: "{{ '{{vpn_99_if_0_static_ipaddr}}' }}" - subnetMask: - optionType: variable - value: "{{ '{{vpn_99_if_0_static_subnet}}' }}" - nat: - optionType: default - value: false - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default - value: true - -lan_vpn_parcel_3: - type: vpn - config: - name: Local_Internet_for_Guests - description: LAN VPN - data: - vpnId: - optionType: global - value: 99 - name: - optionType: global - value: Local Internet for Guests - ipv4Route: [] - enableSdra: - optionType: global - value: false - sub_parcels: - - "{{ lan_interface_ethernet_parcel_3 }}" - -lan_interface_ethernet_parcel_4: - type: ethernet - config: - name: VPN_Corporate_Users_10_Interface - description: LAN Interface - data: - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{vpn_10_if_0}}' }}" - description: - optionType: default - intfIpAddress: - static: - staticIpV4AddressPrimary: - ipAddress: - optionType: variable - value: "{{ '{{vpn_10_if_0_static_ipaddr}}' }}" - subnetMask: - optionType: variable - value: "{{ '{{vpn_10_if_0_static_subnet}}' }}" - nat: - optionType: default - value: false - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default - value: true - -lan_vpn_parcel_4: - type: vpn - config: - name: Corporate_Users - description: LAN VPN - data: - vpnId: - optionType: global - value: 10 - name: - optionType: global - value: Corporate Users - ipv4Route: [] - enableSdra: - optionType: global - value: false - sub_parcels: - - "{{ lan_interface_ethernet_parcel_4 }}" - - -### CONFIG GROUP AND FEATURE PROFILES - -config_group_name: "ansible-generated-cg" -config_group_description: "Ansible Generated Config Group" - -system_profile: - name: "{{ config_group_name }}_Basic" - description: "{{ config_group_name }} Basic Profile" - parcels: - - "{{ banner_parcel }}" - - "{{ basic_parcel }}" - - "{{ bfd_parcel }}" - - "{{ omp_parcel }}" - - "{{ logging_parcel }}" - - "{{ ntp_parcel }}" - - "{{ global_parcel }}" - - "{{ aaa_parcel }}" - - "{{ mrf_parcel }}" - -transport_profile: - name: "{{ config_group_name }}_WAN" - description: "{{ config_group_name }} Transport Profile" - parcels: - - "{{ wan_vpn_parcel }}" - -service_profile: - name: "{{ config_group_name }}_LAN" - description: "{{ config_group_name }} Service Profile" - parcels: - - "{{ lan_vpn_parcel_1 }}" - - "{{ lan_vpn_parcel_2 }}" - - "{{ lan_vpn_parcel_3 }}" - - "{{ lan_vpn_parcel_4 }}" - -config_group_system_profiles: - - "{{ system_profile }}" -config_group_transport_profiles: - - "{{ transport_profile }}" -config_group_service_profiles: - - "{{ service_profile }}" +### SYSTEM PARCELS + +banner_parcel: + type: banner + config: + name: Banner + description: Banner Description + data: + login: + optionType: default + value: "" + motd: + optionType: default + value: "" + +basic_parcel: + type: basic + config: + name: Basic + description: Basic Setting Description + data: + affinityGroupNumber: + optionType: default + affinityGroupPreference: + optionType: default + affinityPerVrf: + - affinityGroupNumber: + optionType: default + vrfRange: + optionType: default + affinityPreferenceAuto: + optionType: default + value: false + siteType: + optionType: default + clock: + timezone: + optionType: default + value: UTC + description: + optionType: default + location: + optionType: default + gpsLocation: + latitude: + optionType: default + longitude: + optionType: default + deviceGroups: + optionType: default + controllerGroupList: + optionType: default + overlayId: + optionType: default + value: 1 + portOffset: + optionType: default + value: 0 + portHop: + optionType: default + value: true + controlSessionPps: + optionType: default + value: 300 + trackTransport: + optionType: default + value: true + trackInterfaceTag: + optionType: default + consoleBaudRate: + optionType: default + value: '9600' + maxOmpSessions: + optionType: default + multiTenant: + optionType: default + value: false + trackDefaultGateway: + optionType: default + value: true + adminTechOnFailure: + optionType: default + value: true + idleTimeout: + optionType: default + onDemand: + onDemandEnable: + optionType: default + value: false + onDemandIdleTimeout: + optionType: default + value: 10 + +bfd_parcel: + type: bfd + config: + name: BFD + description: BFD Description + data: + pollInterval: + optionType: default + value: 600000 + multiplier: + optionType: default + value: 6 + defaultDscp: + optionType: default + value: 48 + +omp_parcel: + type: omp + config: + name: OMP + description: OMP Description + data: + gracefulRestart: + optionType: default + value: true + sendPathLimit: + optionType: default + value: 4 + ecmpLimit: + optionType: default + value: 4 + advertisementInterval: + optionType: default + value: 1 + holdtime: + optionType: default + value: 60 + eorTimer: + optionType: default + value: 300 + overlayAs: + optionType: default + shutdown: + optionType: default + value: false + gracefulRestartTimer: + optionType: default + value: 43200 + ompAdminDistanceIpv4: + optionType: default + value: 251 + ompAdminDistanceIpv6: + optionType: default + value: 251 + advertiseIpv4: + bgp: + optionType: default + value: false + ospf: + optionType: default + value: false + ospfv3: + optionType: default + value: false + connected: + optionType: default + value: true + static: + optionType: default + value: true + eigrp: + optionType: default + value: false + lisp: + optionType: default + value: false + isis: + optionType: default + value: false + advertiseIpv6: + bgp: + optionType: default + value: false + ospf: + optionType: default + value: false + static: + optionType: default + value: false + eigrp: + optionType: default + value: false + connected: + optionType: default + value: false + lisp: + optionType: default + value: false + isis: + optionType: default + value: false + ignoreRegionPathLength: + optionType: default + value: false + transportGateway: + optionType: default + siteTypesForTransportGateway: + optionType: default + +logging_parcel: + type: logging + config: + name: Logging + description: Logging Description + data: + disk: + file: + diskFileSize: + optionType: default + value: 10 + diskFileRotate: + optionType: default + value: 10 + +ntp_parcel: + type: ntp + config: + name: NTP + description: NTP Description + data: + server: + - name: + optionType: global + value: time.google.com + vpn: + optionType: default + value: 0 + version: + optionType: default + value: 4 + key: + optionType: default + sourceInterface: + optionType: default + prefer: + optionType: default + value: false + +global_parcel: + type: global + config: + name: Global + description: Global Description + data: + services_global: + services_ip: + servicesGlobalServicesIpHttpServer: + optionType: default + value: false + servicesGlobalServicesIpHttpsServer: + optionType: default + value: false + servicesGlobalServicesIpFtpPassive: + optionType: default + value: false + servicesGlobalServicesIpDomainLookup: + optionType: default + value: false + servicesGlobalServicesIpArpProxy: + optionType: default + value: false + servicesGlobalServicesIpRcmd: + optionType: default + value: false + servicesGlobalServicesIpLineVty: + optionType: default + value: false + servicesGlobalServicesIpCdp: + optionType: default + value: true + servicesGlobalServicesIpLldp: + optionType: default + value: true + servicesGlobalServicesIpSourceIntrf: + optionType: default + globalOtherSettingsTcpKeepalivesIn: + optionType: default + value: true + globalOtherSettingsTcpKeepalivesOut: + optionType: default + value: true + globalOtherSettingsTcpSmallServers: + optionType: default + value: false + globalOtherSettingsUdpSmallServers: + optionType: default + value: false + globalOtherSettingsConsoleLogging: + optionType: default + value: true + globalOtherSettingsIPSourceRoute: + optionType: default + value: false + globalOtherSettingsVtyLineLogging: + optionType: default + value: false + globalOtherSettingsSnmpIfindexPersist: + optionType: default + value: true + globalOtherSettingsIgnoreBootp: + optionType: default + value: true + globalSettingsNat64UdpTimeout: + optionType: default + value: 300 + globalSettingsNat64TcpTimeout: + optionType: default + value: 3600 + globalSettingsHttpAuthentication: + optionType: default + globalSettingsSSHVersion: + optionType: default + +aaa_parcel: + type: aaa + config: + name: AAA + description: AAA Profile Feature Description + data: + authenticationGroup: + optionType: default + value: false + accountingGroup: + optionType: default + value: false + serverAuthOrder: + optionType: global + value: + - local + authorizationConsole: + optionType: default + value: false + authorizationConfigCommands: + optionType: default + value: false + user: + - name: + optionType: global + value: admin + password: + optionType: variable + value: "{{ '{{admin_password}}' }}" + privilege: + optionType: default + value: '15' + +mrf_parcel: + type: mrf + config: + name: MRF + description: Multi Region Fabric Description + data: + role: + optionType: global + value: edge-router + +### TRANSPORT PARCELS + +wan_interface_ethernet_parcel_1: + type: ethernet + config: + data: + multiRegionFabric: + coreRegion: + optionType: default + value: core-shared + enableCoreRegion: + optionType: default + value: false + enableSecondaryRegion: + optionType: default + value: false + secondaryRegion: + optionType: default + value: secondary-shared + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{vpn_0_mpls_if}}' }}" + description: + optionType: global + value: WAN VPN 0 Interface - MPLS + blockNonSourceIp: + optionType: default + value: false + autoDetectBandwidth: + optionType: default + value: false + tunnelInterface: + optionType: global + value: true + tunnel: + perTunnelQos: + optionType: default + value: false + color: + optionType: global + value: mpls + restrict: + optionType: default + value: false + group: + optionType: default + border: + optionType: default + value: false + maxControlConnections: + optionType: default + vBondAsStunServer: + optionType: default + value: false + excludeControllerGroupList: + optionType: default + vManageConnectionPreference: + optionType: default + value: 5 + portHop: + optionType: default + value: true + lowBandwidthLink: + optionType: default + value: false + tunnelTcpMss: + optionType: default + clearDontFragment: + optionType: default + value: false + ctsSgtPropagation: + optionType: default + value: false + networkBroadcast: + optionType: default + value: false + allowService: + all: + optionType: default + value: false + bgp: + optionType: default + value: false + dhcp: + optionType: default + value: true + ntp: + optionType: default + value: true + ssh: + optionType: default + value: true + dns: + optionType: default + value: true + icmp: + optionType: default + value: true + https: + optionType: default + value: true + ospf: + optionType: default + value: false + stun: + optionType: default + value: false + snmp: + optionType: default + value: false + netconf: + optionType: default + value: false + bfd: + optionType: default + value: false + nat: + optionType: default + value: false + intfIpAddress: + dynamic: + dynamicDhcpDistance: + optionType: default + value: 1 + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default + value: true + encapsulation: + - encap: + optionType: global + value: ipsec + weight: + optionType: default + value: 1 + preference: + optionType: default + name: MPLS + description: WAN VPN 0 MPLS Feature + +wan_interface_ethernet_parcel_2: + type: ethernet + config: + data: + multiRegionFabric: + coreRegion: + optionType: default + value: core-shared + enableCoreRegion: + optionType: default + value: false + enableSecondaryRegion: + optionType: default + value: false + secondaryRegion: + optionType: default + value: secondary-shared + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{vpn_0_public-internet_if}}' }}" + description: + optionType: global + value: WAN VPN 0 Interface - Internet + blockNonSourceIp: + optionType: default + value: false + autoDetectBandwidth: + optionType: default + value: false + tunnelInterface: + optionType: global + value: true + tunnel: + perTunnelQos: + optionType: default + value: false + color: + optionType: global + value: public-internet + restrict: + optionType: default + value: false + group: + optionType: default + border: + optionType: default + value: false + maxControlConnections: + optionType: default + vBondAsStunServer: + optionType: default + value: false + excludeControllerGroupList: + optionType: default + vManageConnectionPreference: + optionType: default + value: 5 + portHop: + optionType: default + value: true + lowBandwidthLink: + optionType: default + value: false + tunnelTcpMss: + optionType: default + clearDontFragment: + optionType: default + value: false + ctsSgtPropagation: + optionType: default + value: false + networkBroadcast: + optionType: default + value: false + allowService: + all: + optionType: default + value: false + bgp: + optionType: default + value: false + dhcp: + optionType: default + value: true + ntp: + optionType: default + value: true + ssh: + optionType: default + value: true + dns: + optionType: default + value: true + icmp: + optionType: default + value: true + https: + optionType: default + value: true + ospf: + optionType: default + value: false + stun: + optionType: default + value: false + snmp: + optionType: default + value: false + netconf: + optionType: default + value: false + bfd: + optionType: default + value: false + nat: + optionType: default + value: false + intfIpAddress: + dynamic: + dynamicDhcpDistance: + optionType: default + value: 1 + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default + value: true + encapsulation: + - encap: + optionType: global + value: ipsec + weight: + optionType: default + value: 1 + preference: + optionType: default + name: Internet + description: WAN VPN 0 Internet Feature + +wan_interface_ethernet_parcel_3: + type: ethernet + config: + data: + multiRegionFabric: + coreRegion: + optionType: default + value: core-shared + enableCoreRegion: + optionType: default + value: false + enableSecondaryRegion: + optionType: default + value: false + secondaryRegion: + optionType: default + value: secondary-shared + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{vpn_0_lte_if}}' }}" + description: + optionType: global + value: WAN VPN 0 Interface - LTE + blockNonSourceIp: + optionType: default + value: false + autoDetectBandwidth: + optionType: default + value: false + tunnelInterface: + optionType: global + value: true + tunnel: + perTunnelQos: + optionType: default + value: false + color: + optionType: global + value: lte + restrict: + optionType: default + value: false + group: + optionType: default + border: + optionType: default + value: false + maxControlConnections: + optionType: default + vBondAsStunServer: + optionType: default + value: false + excludeControllerGroupList: + optionType: default + vManageConnectionPreference: + optionType: default + value: 5 + portHop: + optionType: default + value: true + lowBandwidthLink: + optionType: default + value: false + tunnelTcpMss: + optionType: default + clearDontFragment: + optionType: default + value: false + ctsSgtPropagation: + optionType: default + value: false + networkBroadcast: + optionType: default + value: false + allowService: + all: + optionType: default + value: false + bgp: + optionType: default + value: false + dhcp: + optionType: default + value: true + ntp: + optionType: default + value: true + ssh: + optionType: default + value: true + dns: + optionType: default + value: true + icmp: + optionType: default + value: true + https: + optionType: default + value: true + ospf: + optionType: default + value: false + stun: + optionType: default + value: false + snmp: + optionType: default + value: false + netconf: + optionType: default + value: false + bfd: + optionType: default + value: false + nat: + optionType: default + value: false + intfIpAddress: + dynamic: + dynamicDhcpDistance: + optionType: default + value: 1 + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default + value: true + encapsulation: + - encap: + optionType: global + value: ipsec + weight: + optionType: default + value: 1 + preference: + optionType: default + name: LTE + description: WAN VPN 0 LTE Feature + +wan_vpn_parcel: + type: vpn + config: + name: VPN0 + description: SDWAN Transport Wan Vpn Feature config + data: + vpnId: + optionType: default + value: 0 + enhanceEcmpKeying: + optionType: global + value: true + ipv4Route: [] + sub_parcels: + - "{{ wan_interface_ethernet_parcel_1 }}" + - "{{ wan_interface_ethernet_parcel_2 }}" + - "{{ wan_interface_ethernet_parcel_3 }}" + +### SERVICE PARCELS + +lan_interface_ethernet_parcel_1: + type: ethernet + config: + name: VPN_Payment_Processing_Network_12_Interface + description: LAN Interface + data: + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{vpn_12_if_0}}' }}" + description: + optionType: default + intfIpAddress: + static: + staticIpV4AddressPrimary: + ipAddress: + optionType: variable + value: "{{ '{{vpn_12_if_0_static_ipaddr}}' }}" + subnetMask: + optionType: variable + value: "{{ '{{vpn_12_if_0_static_subnet}}' }}" + nat: + optionType: default + value: false + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default + value: true + +lan_vpn_parcel_1: + type: vpn + config: + name: Payment_Processing_Network + description: LAN VPN + data: + vpnId: + optionType: global + value: 12 + name: + optionType: global + value: Payment Processing Network + ipv4Route: [] + enableSdra: + optionType: global + value: false + sub_parcels: + - "{{ lan_interface_ethernet_parcel_1 }}" + +lan_interface_ethernet_parcel_2: + type: ethernet + config: + name: VPN_Physical_Security_Devices_13_Interface + description: LAN Interface + data: + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{vpn_13_if_0}}' }}" + description: + optionType: default + intfIpAddress: + static: + staticIpV4AddressPrimary: + ipAddress: + optionType: variable + value: "{{ '{{vpn_13_if_0_static_ipaddr}}' }}" + subnetMask: + optionType: variable + value: "{{ '{{vpn_13_if_0_static_subnet}}' }}" + nat: + optionType: default + value: false + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default + value: true + +lan_vpn_parcel_2: + type: vpn + config: + name: Physical_Security_Devices + description: LAN VPN + data: + vpnId: + optionType: global + value: 13 + name: + optionType: global + value: Physical Security Devices + ipv4Route: [] + enableSdra: + optionType: global + value: false + sub_parcels: + - "{{ lan_interface_ethernet_parcel_2 }}" + +lan_interface_ethernet_parcel_3: + type: ethernet + config: + name: VPN_Local_Internet_for_Guests_99_Interface + description: LAN Interface + data: + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{vpn_99_if_0}}' }}" + description: + optionType: default + intfIpAddress: + static: + staticIpV4AddressPrimary: + ipAddress: + optionType: variable + value: "{{ '{{vpn_99_if_0_static_ipaddr}}' }}" + subnetMask: + optionType: variable + value: "{{ '{{vpn_99_if_0_static_subnet}}' }}" + nat: + optionType: default + value: false + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default + value: true + +lan_vpn_parcel_3: + type: vpn + config: + name: Local_Internet_for_Guests + description: LAN VPN + data: + vpnId: + optionType: global + value: 99 + name: + optionType: global + value: Local Internet for Guests + ipv4Route: [] + enableSdra: + optionType: global + value: false + sub_parcels: + - "{{ lan_interface_ethernet_parcel_3 }}" + +lan_interface_ethernet_parcel_4: + type: ethernet + config: + name: VPN_Corporate_Users_10_Interface + description: LAN Interface + data: + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{vpn_10_if_0}}' }}" + description: + optionType: default + intfIpAddress: + static: + staticIpV4AddressPrimary: + ipAddress: + optionType: variable + value: "{{ '{{vpn_10_if_0_static_ipaddr}}' }}" + subnetMask: + optionType: variable + value: "{{ '{{vpn_10_if_0_static_subnet}}' }}" + nat: + optionType: default + value: false + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default + value: true + +lan_vpn_parcel_4: + type: vpn + config: + name: Corporate_Users + description: LAN VPN + data: + vpnId: + optionType: global + value: 10 + name: + optionType: global + value: Corporate Users + ipv4Route: [] + enableSdra: + optionType: global + value: false + sub_parcels: + - "{{ lan_interface_ethernet_parcel_4 }}" + + +### CONFIG GROUP AND FEATURE PROFILES + +config_group_name: "ansible-generated-cg" +config_group_description: "Ansible Generated Config Group" + +system_profile: + name: "{{ config_group_name }}_Basic" + description: "{{ config_group_name }} Basic Profile" + parcels: + - "{{ banner_parcel }}" + - "{{ basic_parcel }}" + - "{{ bfd_parcel }}" + - "{{ omp_parcel }}" + - "{{ logging_parcel }}" + - "{{ ntp_parcel }}" + - "{{ global_parcel }}" + - "{{ aaa_parcel }}" + - "{{ mrf_parcel }}" + +transport_profile: + name: "{{ config_group_name }}_WAN" + description: "{{ config_group_name }} Transport Profile" + parcels: + - "{{ wan_vpn_parcel }}" + +service_profile: + name: "{{ config_group_name }}_LAN" + description: "{{ config_group_name }} Service Profile" + parcels: + - "{{ lan_vpn_parcel_1 }}" + - "{{ lan_vpn_parcel_2 }}" + - "{{ lan_vpn_parcel_3 }}" + - "{{ lan_vpn_parcel_4 }}" + +config_group_system_profiles: + - "{{ system_profile }}" +config_group_transport_profiles: + - "{{ transport_profile }}" +config_group_service_profiles: + - "{{ service_profile }}" diff --git a/playbooks/tests/test_cedge_software_upgrades_local.yml b/playbooks/tests/test_cedge_software_upgrades_local.yml index 3fa0ae2..0a6893f 100644 --- a/playbooks/tests/test_cedge_software_upgrades_local.yml +++ b/playbooks/tests/test_cedge_software_upgrades_local.yml @@ -4,7 +4,7 @@ --- # Helper playbooks to test modules and flows while developing them -# It assumes that controllers are <20.13 version and Edge are <17.13 version +# Run the Manager release compatibility smoke test before destructive upgrade testing. # This playbook should be reused for CI/CD integration. For this, only vars should be adjusted. diff --git a/playbooks/tests/test_cedge_software_upgrades_remote.yml b/playbooks/tests/test_cedge_software_upgrades_remote.yml index 1722c67..e37e323 100644 --- a/playbooks/tests/test_cedge_software_upgrades_remote.yml +++ b/playbooks/tests/test_cedge_software_upgrades_remote.yml @@ -4,7 +4,7 @@ --- # Helper playbooks to test modules and flows while developing them -# It assumes that controllers are <20.13 version and Edge are <17.13 version +# Run the Manager release compatibility smoke test before destructive upgrade testing. # This playbook should be reused for CI/CD integration. For this, only vars should be adjusted. diff --git a/playbooks/tests/test_manager_release_compatibility.yml b/playbooks/tests/test_manager_release_compatibility.yml new file mode 100644 index 0000000..625d9f2 --- /dev/null +++ b/playbooks/tests/test_manager_release_compatibility.yml @@ -0,0 +1,31 @@ +--- +- name: Verify compatibility with the target Cisco Catalyst SD-WAN Manager release + hosts: localhost + gather_facts: false + vars: + target_manager_release: "26.1" + tasks: + - name: Read Manager server information + cisco.catalystwan.server_info: + information_category: server_info + manager_authentication: "{{ manager_authentication }}" + register: manager_server_info + + - name: Verify the Manager release meets the compatibility target + ansible.builtin.assert: + that: + - manager_server_info.response.platform_version is version(target_manager_release, '>=') + fail_msg: >- + Expected Cisco Catalyst SD-WAN Manager {{ target_manager_release }} or newer, + got {{ manager_server_info.response.platform_version | default('unknown') }}. + + - name: Verify the Manager API is ready + cisco.catalystwan.server_info: + information_category: server_ready + manager_authentication: "{{ manager_authentication }}" + register: manager_ready + + - name: Assert that the Manager API reports ready + ansible.builtin.assert: + that: + - manager_ready.response.is_server_ready | bool diff --git a/playbooks/tests/test_module_cluster_management.yml b/playbooks/tests/test_module_cluster_management.yml index b6f2d0c..5a4749c 100644 --- a/playbooks/tests/test_module_cluster_management.yml +++ b/playbooks/tests/test_module_cluster_management.yml @@ -1,49 +1,49 @@ -# Copyright 2024 Cisco Systems, Inc. and its affiliates -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - ---- -- name: Testing playbook to verify cisco.catalystwan.cluster - hosts: localhost - gather_facts: false - vars_files: - - configuration_file_dev_vars.yml - tasks: - - name: "Edit cluster IP address for vManage {{ (vmanage_instances | first).hostname }}" - cisco.catalystwan.cluster_management: - wait_until_configured_seconds: 300 - vmanage_id: "0" - system_ip: "{{ (vmanage_instances | first).system_ip }}" - cluster_ip: "{{ (vmanage_instances | first).cluster_private_ip }}" - username: "{{ (vmanage_instances | first).admin_username }}" - password: "{{ (vmanage_instances | first).admin_password }}" - persona: "{{ (vmanage_instances | first).persona }}" - services: - sd-avc: - server: false - manager_authentication: - url: "{{ (vmanage_instances | first).mgmt_public_ip }}" - username: "{{ (vmanage_instances | first).admin_username }}" - password: "{{ (vmanage_instances | first).admin_password }}" - - - name: Add remaining instances to cluster - cisco.catalystwan.cluster_management: - wait_until_configured_seconds: 1800 - system_ip: "{{ vmanage.system_ip }}" - cluster_ip: "{{ vmanage.cluster_private_ip }}" - username: "{{ vmanage.admin_username }}" - password: "{{ vmanage.admin_password }}" - gen_csr: false - persona: "{{ vmanage.persona }}" - services: - sd-avc: - server: false - manager_authentication: - url: "{{ (vmanage_instances | first).mgmt_public_ip }}" - username: "{{ (vmanage_instances | first).admin_username }}" - password: "{{ (vmanage_instances | first).admin_password }}" - loop: "{{ vmanage_instances[1:] }}" - loop_control: - loop_var: vmanage - when: vmanage.cluster_private_ip is defined - retries: 180 - delay: 10 +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: Testing playbook to verify cisco.catalystwan.cluster + hosts: localhost + gather_facts: false + vars_files: + - configuration_file_dev_vars.yml + tasks: + - name: "Edit cluster IP address for vManage {{ (vmanage_instances | first).hostname }}" + cisco.catalystwan.cluster_management: + wait_until_configured_seconds: 300 + vmanage_id: "0" + system_ip: "{{ (vmanage_instances | first).system_ip }}" + cluster_ip: "{{ (vmanage_instances | first).cluster_private_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + persona: "{{ (vmanage_instances | first).persona }}" + services: + sd-avc: + server: false + manager_authentication: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + + - name: Add remaining instances to cluster + cisco.catalystwan.cluster_management: + wait_until_configured_seconds: 1800 + system_ip: "{{ vmanage.system_ip }}" + cluster_ip: "{{ vmanage.cluster_private_ip }}" + username: "{{ vmanage.admin_username }}" + password: "{{ vmanage.admin_password }}" + gen_csr: false + persona: "{{ vmanage.persona }}" + services: + sd-avc: + server: false + manager_authentication: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" + loop: "{{ vmanage_instances[1:] }}" + loop_control: + loop_var: vmanage + when: vmanage.cluster_private_ip is defined + retries: 180 + delay: 10 diff --git a/playbooks/tests/test_module_config_groups.yml b/playbooks/tests/test_module_config_groups.yml index 9fec960..d992f7d 100644 --- a/playbooks/tests/test_module_config_groups.yml +++ b/playbooks/tests/test_module_config_groups.yml @@ -1,22 +1,22 @@ -# Copyright 2024 Cisco Systems, Inc. and its affiliates -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - ---- -- name: Testing playbook to verify cisco.catalystwan.config_groups - hosts: localhost - gather_facts: false - vars_files: - - configuration_file_dev_vars.yml - - data/config_groups.yml - tasks: - - name: "Create config group" - cisco.catalystwan.config_groups: - name: "{{ config_group_name }}" - description: "{{ config_group_description }}" - system_profiles: "{{ config_group_system_profiles }}" - transport_profiles: "{{ config_group_transport_profiles }}" - service_profiles: "{{ config_group_service_profiles }}" - manager_authentication: - url: "{{ (vmanage_instances | first).mgmt_public_ip }}" - username: "{{ (vmanage_instances | first).admin_username }}" - password: "{{ (vmanage_instances | first).admin_password }}" +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: Testing playbook to verify cisco.catalystwan.config_groups + hosts: localhost + gather_facts: false + vars_files: + - configuration_file_dev_vars.yml + - data/config_groups.yml + tasks: + - name: "Create config group" + cisco.catalystwan.config_groups: + name: "{{ config_group_name }}" + description: "{{ config_group_description }}" + system_profiles: "{{ config_group_system_profiles }}" + transport_profiles: "{{ config_group_transport_profiles }}" + service_profiles: "{{ config_group_service_profiles }}" + manager_authentication: + url: "{{ (vmanage_instances | first).mgmt_public_ip }}" + username: "{{ (vmanage_instances | first).admin_username }}" + password: "{{ (vmanage_instances | first).admin_password }}" diff --git a/playbooks/tests/test_vbond_software_upgrades_local.yml b/playbooks/tests/test_vbond_software_upgrades_local.yml index 540c485..4681e44 100644 --- a/playbooks/tests/test_vbond_software_upgrades_local.yml +++ b/playbooks/tests/test_vbond_software_upgrades_local.yml @@ -4,7 +4,7 @@ --- # Helper playbooks to test modules and flows while developing them -# It assumes that controllers are <20.13 version and Edge are <17.13 version +# Run the Manager release compatibility smoke test before destructive upgrade testing. # This playbook should be reused for CI/CD integration. For this, only vars should be adjusted. diff --git a/playbooks/tests/test_vbond_software_upgrades_remote.yml b/playbooks/tests/test_vbond_software_upgrades_remote.yml index 3de9e8a..913aef7 100644 --- a/playbooks/tests/test_vbond_software_upgrades_remote.yml +++ b/playbooks/tests/test_vbond_software_upgrades_remote.yml @@ -4,7 +4,7 @@ --- # Helper playbooks to test modules and flows while developing them -# It assumes that controllers are <20.13 version and Edge are <17.13 version +# Run the Manager release compatibility smoke test before destructive upgrade testing. # This playbook should be reused for CI/CD integration. For this, only vars should be adjusted. diff --git a/playbooks/tests/test_vmanage_software_upgrades_local.yml b/playbooks/tests/test_vmanage_software_upgrades_local.yml index 0af411f..431c313 100644 --- a/playbooks/tests/test_vmanage_software_upgrades_local.yml +++ b/playbooks/tests/test_vmanage_software_upgrades_local.yml @@ -4,7 +4,7 @@ --- # Helper playbooks to test modules and flows while developing them -# It assumes that controllers are <20.13 version and Edge are <17.13 version +# Run the Manager release compatibility smoke test before destructive upgrade testing. # This playbook should be reused for CI/CD integration. For this, only vars should be adjusted. # Does not support cluster yet. diff --git a/playbooks/tests/test_vmanage_software_upgrades_remote.yml b/playbooks/tests/test_vmanage_software_upgrades_remote.yml index b58a844..7f75ee7 100644 --- a/playbooks/tests/test_vmanage_software_upgrades_remote.yml +++ b/playbooks/tests/test_vmanage_software_upgrades_remote.yml @@ -4,7 +4,7 @@ --- # Helper playbooks to test modules and flows while developing them -# It assumes that controllers are <20.13 version and Edge are <17.13 version +# Run the Manager release compatibility smoke test before destructive upgrade testing. # This playbook should be reused for CI/CD integration. For this, only vars should be adjusted. # Does not support cluster yet. diff --git a/playbooks/tests/test_vsmart_software_upgrades_local.yml b/playbooks/tests/test_vsmart_software_upgrades_local.yml index 0181b37..23e0ad3 100644 --- a/playbooks/tests/test_vsmart_software_upgrades_local.yml +++ b/playbooks/tests/test_vsmart_software_upgrades_local.yml @@ -4,7 +4,7 @@ --- # Helper playbooks to test modules and flows while developing them -# It assumes that controllers are <20.13 version and Edge are <17.13 version +# Run the Manager release compatibility smoke test before destructive upgrade testing. # This playbook should be reused for CI/CD integration. For this, only vars should be adjusted. diff --git a/playbooks/tests/test_vsmart_software_upgrades_remote.yml b/playbooks/tests/test_vsmart_software_upgrades_remote.yml index 2229170..3b65304 100644 --- a/playbooks/tests/test_vsmart_software_upgrades_remote.yml +++ b/playbooks/tests/test_vsmart_software_upgrades_remote.yml @@ -4,7 +4,7 @@ --- # Helper playbooks to test modules and flows while developing them -# It assumes that controllers are <20.13 version and Edge are <17.13 version +# Run the Manager release compatibility smoke test before destructive upgrade testing. # Tested operations: diff --git a/plugins/doc_fragments/device_models_device_template.py b/plugins/doc_fragments/device_models_device_template.py index cde723c..01f3f3c 100644 --- a/plugins/doc_fragments/device_models_device_template.py +++ b/plugins/doc_fragments/device_models_device_template.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/device_models_feature_template.py b/plugins/doc_fragments/device_models_feature_template.py index f9ff45d..b28a0ca 100644 --- a/plugins/doc_fragments/device_models_feature_template.py +++ b/plugins/doc_fragments/device_models_feature_template.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_aaa.py b/plugins/doc_fragments/feature_template_aaa.py index 6047db1..aee94ef 100644 --- a/plugins/doc_fragments/feature_template_aaa.py +++ b/plugins/doc_fragments/feature_template_aaa.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_aaa.py b/plugins/doc_fragments/feature_template_cisco_aaa.py index 0d1ed35..ada93e3 100644 --- a/plugins/doc_fragments/feature_template_cisco_aaa.py +++ b/plugins/doc_fragments/feature_template_cisco_aaa.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_banner.py b/plugins/doc_fragments/feature_template_cisco_banner.py index 458aea4..53d8c52 100644 --- a/plugins/doc_fragments/feature_template_cisco_banner.py +++ b/plugins/doc_fragments/feature_template_cisco_banner.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_bfd.py b/plugins/doc_fragments/feature_template_cisco_bfd.py index 4c7867c..9066665 100644 --- a/plugins/doc_fragments/feature_template_cisco_bfd.py +++ b/plugins/doc_fragments/feature_template_cisco_bfd.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_logging.py b/plugins/doc_fragments/feature_template_cisco_logging.py index 47434cb..916c19f 100644 --- a/plugins/doc_fragments/feature_template_cisco_logging.py +++ b/plugins/doc_fragments/feature_template_cisco_logging.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_ntp.py b/plugins/doc_fragments/feature_template_cisco_ntp.py index dc662a9..24c407f 100644 --- a/plugins/doc_fragments/feature_template_cisco_ntp.py +++ b/plugins/doc_fragments/feature_template_cisco_ntp.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_omp.py b/plugins/doc_fragments/feature_template_cisco_omp.py index 3024d07..40acb02 100644 --- a/plugins/doc_fragments/feature_template_cisco_omp.py +++ b/plugins/doc_fragments/feature_template_cisco_omp.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_ospf.py b/plugins/doc_fragments/feature_template_cisco_ospf.py index e56b9b8..d809cf7 100644 --- a/plugins/doc_fragments/feature_template_cisco_ospf.py +++ b/plugins/doc_fragments/feature_template_cisco_ospf.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_secure_internet_gateway.py b/plugins/doc_fragments/feature_template_cisco_secure_internet_gateway.py index f93c7cc..0a4581b 100644 --- a/plugins/doc_fragments/feature_template_cisco_secure_internet_gateway.py +++ b/plugins/doc_fragments/feature_template_cisco_secure_internet_gateway.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_snmp.py b/plugins/doc_fragments/feature_template_cisco_snmp.py index c497c0e..aa04558 100644 --- a/plugins/doc_fragments/feature_template_cisco_snmp.py +++ b/plugins/doc_fragments/feature_template_cisco_snmp.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_cisco_system.py b/plugins/doc_fragments/feature_template_cisco_system.py index 4edee17..16f666f 100644 --- a/plugins/doc_fragments/feature_template_cisco_system.py +++ b/plugins/doc_fragments/feature_template_cisco_system.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates @@ -454,7 +453,7 @@ class ModuleDocFragment(object): suboptions: name: default: system_host_name - required: true + required: false type: str description: Device Specific Variables name location: @@ -530,7 +529,7 @@ class ModuleDocFragment(object): suboptions: name: default: system_system_ip - required: true + required: false type: str description: Device Specific Variables name overlay_id: @@ -543,7 +542,7 @@ class ModuleDocFragment(object): description: - The site ID of the device. required: false - default: system_site_id + default: null type: int site_type: description: diff --git a/plugins/doc_fragments/feature_template_cisco_vpn.py b/plugins/doc_fragments/feature_template_cisco_vpn.py index 29dc1a0..d25de23 100644 --- a/plugins/doc_fragments/feature_template_cisco_vpn.py +++ b/plugins/doc_fragments/feature_template_cisco_vpn.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates @@ -270,7 +269,6 @@ class ModuleDocFragment(object): required: false default: null type: dict - elements: dict suboptions: interface_name: description: diff --git a/plugins/doc_fragments/feature_template_cisco_vpn_interface.py b/plugins/doc_fragments/feature_template_cisco_vpn_interface.py index 792a08e..e5559d7 100644 --- a/plugins/doc_fragments/feature_template_cisco_vpn_interface.py +++ b/plugins/doc_fragments/feature_template_cisco_vpn_interface.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_omp_vsmart.py b/plugins/doc_fragments/feature_template_omp_vsmart.py index 73a48e3..05607e9 100644 --- a/plugins/doc_fragments/feature_template_omp_vsmart.py +++ b/plugins/doc_fragments/feature_template_omp_vsmart.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_security_vsmart.py b/plugins/doc_fragments/feature_template_security_vsmart.py index 20a6182..8424149 100644 --- a/plugins/doc_fragments/feature_template_security_vsmart.py +++ b/plugins/doc_fragments/feature_template_security_vsmart.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/feature_template_system_vsmart.py b/plugins/doc_fragments/feature_template_system_vsmart.py index dc70f37..8a298d7 100644 --- a/plugins/doc_fragments/feature_template_system_vsmart.py +++ b/plugins/doc_fragments/feature_template_system_vsmart.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates @@ -448,7 +447,7 @@ class ModuleDocFragment(object): suboptions: name: default: system_host_name - required: true + required: false type: str description: Device Specific Variables name dual_stack_ipv6: @@ -459,7 +458,7 @@ class ModuleDocFragment(object): suboptions: name: default: system_ipv6-strict-control - required: true + required: false type: str description: Device Specific Variables name description: @@ -507,7 +506,7 @@ class ModuleDocFragment(object): suboptions: name: default: system_system_ip - required: true + required: false type: str description: Device Specific Variables name site_id: @@ -518,7 +517,7 @@ class ModuleDocFragment(object): suboptions: name: default: system_site_id - required: true + required: false type: str description: Device Specific Variables name overlay_id: diff --git a/plugins/doc_fragments/feature_template_vpn_vsmart.py b/plugins/doc_fragments/feature_template_vpn_vsmart.py index 6b35787..9dd8784 100644 --- a/plugins/doc_fragments/feature_template_vpn_vsmart.py +++ b/plugins/doc_fragments/feature_template_vpn_vsmart.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates @@ -117,7 +116,6 @@ class ModuleDocFragment(object): required: false default: null type: dict - elements: dict suboptions: interface_name: description: diff --git a/plugins/doc_fragments/feature_template_vpn_vsmart_interface.py b/plugins/doc_fragments/feature_template_vpn_vsmart_interface.py index f8f65c0..82bc11b 100644 --- a/plugins/doc_fragments/feature_template_vpn_vsmart_interface.py +++ b/plugins/doc_fragments/feature_template_vpn_vsmart_interface.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/plugins/doc_fragments/manager_authentication.py b/plugins/doc_fragments/manager_authentication.py index 7087530..7f6d585 100644 --- a/plugins/doc_fragments/manager_authentication.py +++ b/plugins/doc_fragments/manager_authentication.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates @@ -33,7 +32,6 @@ class ModuleDocFragment(object): - Password for authentication with vManage. required: true type: str - no_log: true port: description: - Port number to use for connecting to vManage. diff --git a/plugins/module_utils/feature_templates/cisco_system.py b/plugins/module_utils/feature_templates/cisco_system.py index 078caa7..cb883a1 100644 --- a/plugins/module_utils/feature_templates/cisco_system.py +++ b/plugins/module_utils/feature_templates/cisco_system.py @@ -27,7 +27,7 @@ "epfr": {"default": None, "required": False, "type": "str"}, "hostname": { "default": None, - "options": {"name": {"default": "system_host_name", "required": True, "type": "str"}}, + "options": {"name": {"default": "system_host_name", "required": False, "type": "str"}}, "required": False, "type": "raw", }, @@ -78,11 +78,11 @@ "region_id": {"default": None, "required": False, "type": "int"}, "role": {"default": None, "required": False, "type": "str"}, "secondary_region": {"default": None, "required": False, "type": "int"}, - "site_id": {"default": "system_site_id", "required": False, "type": "int"}, + "site_id": {"default": None, "required": False, "type": "int"}, "site_type": {"default": None, "elements": "str", "required": False, "type": "list"}, "system_ip": { "default": None, - "options": {"name": {"default": "system_system_ip", "required": True, "type": "str"}}, + "options": {"name": {"default": "system_system_ip", "required": False, "type": "str"}}, "required": False, "type": "raw", }, diff --git a/plugins/module_utils/feature_templates/system_vsmart.py b/plugins/module_utils/feature_templates/system_vsmart.py index fa59be1..37f74af 100644 --- a/plugins/module_utils/feature_templates/system_vsmart.py +++ b/plugins/module_utils/feature_templates/system_vsmart.py @@ -20,13 +20,13 @@ "dns_cache_timeout": {"default": None, "required": False, "type": "int"}, "dual_stack_ipv6": { "default": None, - "options": {"name": {"default": "system_ipv6-strict-control", "required": True, "type": "str"}}, + "options": {"name": {"default": "system_ipv6-strict-control", "required": False, "type": "str"}}, "required": False, "type": "raw", }, "host_name": { "default": None, - "options": {"name": {"default": "system_host_name", "required": True, "type": "str"}}, + "options": {"name": {"default": "system_host_name", "required": False, "type": "str"}}, "required": False, "type": "raw", }, @@ -52,13 +52,13 @@ "region_list_id": {"default": None, "required": False, "type": "int"}, "site_id": { "default": None, - "options": {"name": {"default": "system_site_id", "required": True, "type": "str"}}, + "options": {"name": {"default": "system_site_id", "required": False, "type": "str"}}, "required": False, "type": "raw", }, "system_ip": { "default": None, - "options": {"name": {"default": "system_system_ip", "required": True, "type": "str"}}, + "options": {"name": {"default": "system_system_ip", "required": False, "type": "str"}}, "required": False, "type": "raw", }, diff --git a/plugins/module_utils/filters.py b/plugins/module_utils/filters.py index 7238e44..cf34d59 100644 --- a/plugins/module_utils/filters.py +++ b/plugins/module_utils/filters.py @@ -61,9 +61,9 @@ def get_vedges_details( module: AnsibleCatalystwanModule, deployed_only: bool, ) -> DataSequence[DeviceDetailsResponse]: - vedge_details: DataSequence[ - DeviceDetailsResponse - ] = module.session.endpoints.configuration_device_inventory.get_device_details(device_category="vedges") + vedge_details: DataSequence[DeviceDetailsResponse] = ( + module.session.endpoints.configuration_device_inventory.get_device_details(device_category="vedges") + ) if not deployed_only: return vedge_details diff --git a/plugins/module_utils/policy_templates/definition.py b/plugins/module_utils/policy_templates/definition.py index ad167dc..10c7553 100644 --- a/plugins/module_utils/policy_templates/definition.py +++ b/plugins/module_utils/policy_templates/definition.py @@ -1,55 +1,37 @@ from typing import Mapping -from catalystwan.models.policy import ( - AclIPv6Policy, - AclPolicy, - AdvancedInspectionProfilePolicy, - AdvancedMalwareProtectionPolicy, - CflowdPolicy, - ControlPolicy, - DeviceAccessIPv6Policy, - DeviceAccessPolicy, - DnsSecurityPolicy, - HubAndSpokePolicy, - IntrusionPreventionPolicy, - MeshPolicy, - QoSMapPolicy, - RewritePolicy, - RoutePolicy, - RuleSet, - SecurityGroup, - SslDecryptionPolicy, - SslDecryptionUtdProfilePolicy, - TrafficDataPolicy, - UrlFilteringPolicy, - VPNMembershipPolicy, - ZoneBasedFWPolicy, -) +from catalystwan.models import policy as policy_models + +_policy_definition_model_names = { + "access_control_list": "AclPolicy", + "access_control_policy_ipv6": "AclIPv6Policy", + "aip": "AdvancedInspectionProfilePolicy", + "amp": "AdvancedMalwareProtectionPolicy", + "cflowd": "CflowdPolicy", + "control": "ControlPolicy", + "device_access": "DeviceAccessPolicy", + "device_access_ipv6": "DeviceAccessIPv6Policy", + "dns_security": "DnsSecurityPolicy", + "hub_and_spoke": "HubAndSpokePolicy", + "intrusion_prevention": "IntrusionPreventionPolicy", + "mesh": "MeshPolicy", + "qos_map": "QoSMapPolicy", + "rewrite": "RewritePolicy", + "route_policy": "RoutePolicy", + "rule_set": "RuleSet", + "security_group": "SecurityGroup", + "ssl_decryption": "SslDecryptionPolicy", + "ssl_decryption_utd_profile": "SslDecryptionUtdProfilePolicy", + "traffic_data": "TrafficDataPolicy", + "url_filtering": "UrlFilteringPolicy", + "vpn_membership": "VPNMembershipPolicy", + "zone_based_firewall": "ZoneBasedFWPolicy", +} policy_definition_type_mapping: Mapping[str, type] = { - "access_control_list": AclPolicy, - "access_control_policy_ipv6": AclIPv6Policy, - "aip": AdvancedInspectionProfilePolicy, - "amp": AdvancedMalwareProtectionPolicy, - "cflowd": CflowdPolicy, - "control": ControlPolicy, - "device_access": DeviceAccessPolicy, - "device_access_ipv6": DeviceAccessIPv6Policy, - "dns_security": DnsSecurityPolicy, - "hub_and_spoke": HubAndSpokePolicy, - "intrusion_prevention": IntrusionPreventionPolicy, - "mesh": MeshPolicy, - "qos_map": QoSMapPolicy, - "rewrite": RewritePolicy, - "route_policy": RoutePolicy, - "rule_set": RuleSet, - "security_group": SecurityGroup, - "ssl_decryption": SslDecryptionPolicy, - "ssl_decryption_utd_profile": SslDecryptionUtdProfilePolicy, - "traffic_data": TrafficDataPolicy, - "url_filtering": UrlFilteringPolicy, - "vpn_membership": VPNMembershipPolicy, - "zone_based_firewall": ZoneBasedFWPolicy, + key: getattr(policy_models, model_name) + for key, model_name in _policy_definition_model_names.items() + if hasattr(policy_models, model_name) } policy_definition_definition = { diff --git a/plugins/module_utils/policy_templates/list.py b/plugins/module_utils/policy_templates/list.py index ffd81f3..d00faef 100644 --- a/plugins/module_utils/policy_templates/list.py +++ b/plugins/module_utils/policy_templates/list.py @@ -1,91 +1,55 @@ from typing import Mapping -from catalystwan.models.policy import ( - AppList, - AppProbeClassList, - ASPathList, - ClassMapList, - ColorList, - CommunityList, - DataIPv6PrefixList, - DataPrefixList, - ExpandedCommunityList, - ExtendedCommunityList, - FaxProtocolList, - FQDNList, - GeoLocationList, - IdentityList, - IPSSignatureList, - IPv6PrefixList, - LocalAppList, - LocalDomainList, - MediaProfileList, - MirrorList, - ModemPassThroughList, - PolicerList, - PortList, - PreferredColorGroupList, - PrefixList, - ProtocolNameList, - RegionList, - ScalableGroupTagList, - SiteList, - SLAClassList, - SupervisoryDisconnectList, - ThreatGridApiKeyList, - TLOCList, - TranslationProfileList, - TranslationRulesList, - TrunkGroupList, - UmbrellaDataList, - URLAllowList, - URLBlockList, - VPNList, - ZoneList, -) +from catalystwan.models import policy as policy_models + +_policy_list_model_names = { + "app": "AppList", + "app_probe": "AppProbeClassList", + "as_path": "ASPathList", + "class_map": "ClassMapList", + "color": "ColorList", + "communities": "CommunityList", + "data_ipv6_prefix": "DataIPv6PrefixList", + "data_prefix": "DataPrefixList", + "expanded_community": "ExpandedCommunityList", + "extended_community": "ExtendedCommunityList", + "fax_protocol": "FaxProtocolList", + "fqdn": "FQDNList", + "geo_location": "GeoLocationList", + "identity": "IdentityList", + "ips_signature": "IPSSignatureList", + "ipv6_prefix": "IPv6PrefixList", + "local_app": "LocalAppList", + "local_domain": "LocalDomainList", + "media_profile": "MediaProfileList", + "mirror": "MirrorList", + "modem_pass_through": "ModemPassThroughList", + "policer": "PolicerList", + "port": "PortList", + "preferred_color_group": "PreferredColorGroupList", + "prefix": "PrefixList", + "protocol_name": "ProtocolNameList", + "region": "RegionList", + "scalable_group_tag": "ScalableGroupTagList", + "site": "SiteList", + "sla": "SLAClassList", + "supervisory_disconnect": "SupervisoryDisconnectList", + "threat_grid_api_key": "ThreatGridApiKeyList", # pragma: allowlist secret + "tloc": "TLOCList", + "translation_profile": "TranslationProfileList", + "translation_rules": "TranslationRulesList", + "trunk_group": "TrunkGroupList", + "umbrella_data": "UmbrellaDataList", + "url_allow": "URLAllowList", + "url_block": "URLBlockList", + "vpn": "VPNList", + "zone": "ZoneList", +} policy_list_type_mapping: Mapping[str, type] = { - "app": AppList, - "app_probe": AppProbeClassList, - "as_path": ASPathList, - "class_map": ClassMapList, - "color": ColorList, - "communities": CommunityList, - "data_ipv6_prefix": DataIPv6PrefixList, - "data_prefix": DataPrefixList, - "expanded_community": ExpandedCommunityList, - "extended_community": ExtendedCommunityList, - "fax_protocol": FaxProtocolList, - "fqdn": FQDNList, - "geo_location": GeoLocationList, - "identity": IdentityList, - "ips_signature": IPSSignatureList, - "ipv6_prefix": IPv6PrefixList, - "local_app": LocalAppList, - "local_domain": LocalDomainList, - "media_profile": MediaProfileList, - "mirror": MirrorList, - "modem_pass_through": ModemPassThroughList, - "policer": PolicerList, - "port": PortList, - "preferred_color_group": PreferredColorGroupList, - "prefix": PrefixList, - "protocol_name": ProtocolNameList, - "region": RegionList, - "scalable_group_tag": ScalableGroupTagList, - "site": SiteList, - "sla": SLAClassList, - "supervisory_disconnect": SupervisoryDisconnectList, - "threat_grid_api_key": ThreatGridApiKeyList, - "tloc": TLOCList, - "translation_profile": TranslationProfileList, - "translation_rules": TranslationRulesList, - "trunk_group": TrunkGroupList, - "umbrella_data": UmbrellaDataList, - "url_allow": URLAllowList, - "url_block": URLBlockList, - "vpn": VPNList, - "zone": ZoneList, + key: getattr(policy_models, model_name) + for key, model_name in _policy_list_model_names.items() + if hasattr(policy_models, model_name) } policy_list_definition = { diff --git a/plugins/module_utils/result.py b/plugins/module_utils/result.py index 274fb0b..4c60f40 100644 --- a/plugins/module_utils/result.py +++ b/plugins/module_utils/result.py @@ -11,7 +11,7 @@ class ModuleResult(BaseModel): model_config = ConfigDict(extra=ALLOW) - response: Optional[Dict] = Field(default={}) # for responses from Manager after running action - state: Optional[Dict] = Field(default={}) # for current state when no changes applied + response: Optional[Dict] = Field(default_factory=dict) # for responses from Manager after running action + state: Optional[Dict] = Field(default_factory=dict) # for current state when no changes applied changed: bool = Field(default=False) msg: Optional[str] = Field(default="") diff --git a/plugins/module_utils/vmanage_module.py b/plugins/module_utils/vmanage_module.py index 230297c..c2f1d66 100644 --- a/plugins/module_utils/vmanage_module.py +++ b/plugins/module_utils/vmanage_module.py @@ -27,7 +27,7 @@ from catalystwan.vmanage_auth import UnauthorizedAccessError HAS_LIB = True -except: # noqa: E722 +except ImportError: HAS_LIB = False LIB_IMP_ERR = traceback.format_exc() @@ -36,8 +36,7 @@ class GetDataFunc(Protocol[ReturnType]): - def __call__(self, **kwargs: Any) -> ReturnType: - ... + def __call__(self, **kwargs: Any) -> ReturnType: ... class AnsibleCatalystwanModule: @@ -78,9 +77,7 @@ def __init__(self, argument_spec=None, supports_check_mode=False, session_reconn log_level = ( logging.ERROR if self.module._verbosity == 0 - else logging.INFO - if self.module._verbosity == 1 - else logging.DEBUG + else logging.INFO if self.module._verbosity == 1 else logging.DEBUG ) self.logger = configure_logger( name="ansible_catalystwan_module", loglevel=log_level, logfile_dir=self.module.params["catalystwan_log_dir"] diff --git a/plugins/modules/active_sessions_info.py b/plugins/modules/active_sessions_info.py index 7fff589..f4961be 100644 --- a/plugins/modules/active_sessions_info.py +++ b/plugins/modules/active_sessions_info.py @@ -36,14 +36,13 @@ - Password for authentication with vManage. required: true type: str - no_log: true port: description: - Port number to use for connecting to vManage. required: false type: str author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) """ EXAMPLES = r""" @@ -96,7 +95,7 @@ class ExtendedModuleResult(ModuleResult): - active_sessions: Optional[List] = Field(default=[]) + active_sessions: Optional[List] = Field(default_factory=list) def run_module(): diff --git a/plugins/modules/administration_settings.py b/plugins/modules/administration_settings.py index 19442a1..d4e5f8f 100644 --- a/plugins/modules/administration_settings.py +++ b/plugins/modules/administration_settings.py @@ -69,7 +69,6 @@ description: Password for Smart Account. type: str required: true - no_log: true username: description: Username for Smart Account. type: str @@ -99,7 +98,7 @@ description: Control PPS, should be in range 300-65535. type: str author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication @@ -208,13 +207,32 @@ from catalystwan.endpoints.configuration_settings import ( Certificate, Device, - EnterpriseRootCA, OnOffMode, Organization, PnPConnectSync, SmartAccountCredentials, SoftwareInstallTimeout, ) +from pydantic import BaseModel, ConfigDict, Field + +try: + from catalystwan.endpoints.configuration_settings import EnterpriseRootCA + + HAS_ENTERPRISE_ROOT_CA_ENDPOINT = True +except ImportError: + HAS_ENTERPRISE_ROOT_CA_ENDPOINT = False + + class EnterpriseRootCA(BaseModel): + """Payload used by SDK versions that no longer expose this endpoint model.""" + + model_config = ConfigDict(populate_by_name=True) + enterprise_root_ca: str = Field(serialization_alias="enterpriseRootCA", validation_alias="enterpriseRootCA") + control_connection_up: bool = Field( + default=False, + serialization_alias="controlConnectionUp", + validation_alias="controlConnectionUp", + ) + from ..module_utils.result import ModuleResult from ..module_utils.vmanage_module import AnsibleCatalystwanModule @@ -342,6 +360,13 @@ def run_module(): modify_certificates = True if certificates_data != certificates_payload else False if module.params.get("enterprise_root_ca"): + if not HAS_ENTERPRISE_ROOT_CA_ENDPOINT: + module.fail_json( + msg=( + "The installed catalystwan SDK does not expose the Enterprise Root CA endpoint. " + "Install catalystwan==0.41.5.dev2 to use this option." + ) + ) enterprise_ca_payload = EnterpriseRootCA( enterprise_root_ca=module.params_without_none_values.get("enterprise_root_ca") ) diff --git a/plugins/modules/alarms.py b/plugins/modules/alarms.py index 75cc888..4da3aba 100644 --- a/plugins/modules/alarms.py +++ b/plugins/modules/alarms.py @@ -22,7 +22,7 @@ - If not provided, the module will retrieve alarms from the beginning of time. type: int required: False - default: None + default: null mark_all_as_viewed: description: - Whether to mark all the alarms as viewed. @@ -41,9 +41,9 @@ - If not provided, alarms will not be logged to a file. type: str required: False - default: None + default: null author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication @@ -75,7 +75,6 @@ - In this case, always false since this module is for information retrieval only. returned: always type: bool - default: False """ EXAMPLES = r""" @@ -114,8 +113,8 @@ class ExtendedModuleResult(ModuleResult): - alarms: Optional[List] = Field(default=[]) - number_of_alarms: Optional[List] = Field(default=[]) + alarms: Optional[List] = Field(default_factory=list) + number_of_alarms: int = 0 def run_module(): @@ -148,9 +147,9 @@ def run_module(): alarms_dict = [asdict(alarm) for alarm in alarms] for alarm in alarms_dict: - if type(alarm["severity"]) is Severity: + if isinstance(alarm["severity"], Severity): alarm["severity"] = alarm["severity"].value - result.alarms = [alarm for alarm in alarms_dict] + result.alarms = list(alarms_dict) result.changed = False result.number_of_alarms = len(alarms_dict) diff --git a/plugins/modules/cli_templates.py b/plugins/modules/cli_templates.py index 78c7042..aff7e96 100644 --- a/plugins/modules/cli_templates.py +++ b/plugins/modules/cli_templates.py @@ -30,7 +30,7 @@ - The description of the CLI template. required: false type: str - default: None + default: null config_file: description: - The path to the configuration file that contains the CLI template content. @@ -48,7 +48,7 @@ - The timeout in seconds for attaching the template. Default is 300. type: int author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.device_models_device_template - cisco.catalystwan.manager_authentication @@ -94,14 +94,32 @@ from typing import List, Literal, Optional, get_args from catalystwan.api.template_api import CLITemplate -from catalystwan.api.templates.device_template.device_template import DeviceTemplateConfigAttached from catalystwan.dataclasses import Device -from catalystwan.models.common import DeviceModel -from catalystwan.models.templates import DeviceTemplateInformation from catalystwan.session import ManagerHTTPError from catalystwan.typed_list import DataSequence from ciscoconfparse import CiscoConfParse # type: ignore -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field + +try: + from catalystwan.api.templates.device_template.device_template import DeviceTemplateConfigAttached +except ImportError: + + class DeviceTemplateConfigAttached(BaseModel): + """Response model removed from the stable 0.40+ SDK line.""" + + model_config = ConfigDict(populate_by_name=True) + uuid: Optional[str] = None + + +try: + from catalystwan.dataclasses import DeviceTemplateInfo +except ImportError: + from catalystwan.models.templates import DeviceTemplateInformation as DeviceTemplateInfo + +try: + from catalystwan.utils.device_model import DeviceModel +except ImportError: + from catalystwan.models.common import DeviceModel from ..module_utils.result import ModuleResult from ..module_utils.vmanage_module import AnsibleCatalystwanModule @@ -154,10 +172,10 @@ def run_module(): template_name = module.params.get("template_name") - all_templates: DataSequence[DeviceTemplateInformation] = module.get_response_safely( + all_templates: DataSequence[DeviceTemplateInfo] = module.get_response_safely( module.session.api.templates.get, template=CLITemplate ) - target_template: Optional[DeviceTemplateInformation] = all_templates.filter(name=template_name) + target_template: Optional[DeviceTemplateInfo] = all_templates.filter(name=template_name) if module.params.get("state") == "present": # Code for checking if template name exists already diff --git a/plugins/modules/cli_templates_info.py b/plugins/modules/cli_templates_info.py index 54a65bd..eaac3e9 100644 --- a/plugins/modules/cli_templates_info.py +++ b/plugins/modules/cli_templates_info.py @@ -12,6 +12,8 @@ version_added: "0.3.5" description: - This module allows you to get CLI Templates Info from vManage. +author: + - Arkadiusz Cichon (@acichon) options: filters: description: @@ -128,9 +130,13 @@ from catalystwan.api.template_api import CLITemplate from catalystwan.api.templates.device_template.device_template import DeviceTemplate -from catalystwan.models.templates import DeviceTemplateInformation from catalystwan.typed_list import DataSequence +try: + from catalystwan.dataclasses import DeviceTemplateInfo +except ImportError: + from catalystwan.models.templates import DeviceTemplateInformation as DeviceTemplateInfo + from ..module_utils.result import ModuleResult from ..module_utils.vmanage_module import AnsibleCatalystwanModule @@ -146,14 +152,14 @@ def run_module(): filters = module.params.get("filters") - all_templates: DataSequence[DeviceTemplateInformation] = module.get_response_safely( + all_templates: DataSequence[DeviceTemplateInfo] = module.get_response_safely( module.session.api.templates.get, template=CLITemplate ) if module.params.get("filters"): - result.templates_info = [template for template in all_templates.filter(**filters)] + result.templates_info = list(all_templates.filter(**filters)) else: - result.templates_info = [template for template in all_templates] + result.templates_info = list(all_templates) if module.params.get("gather_configuration"): result.templates_configuration = [] diff --git a/plugins/modules/cluster_management.py b/plugins/modules/cluster_management.py index 90f9b09..8614e20 100644 --- a/plugins/modules/cluster_management.py +++ b/plugins/modules/cluster_management.py @@ -1,323 +1,356 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright 2024 Cisco Systems, Inc. and its affiliates -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - - -DOCUMENTATION = r""" ---- -module: cluster_management -short_description: Cluster configuration for vManage devices -version_added: "0.2.1" -description: This module can be used to add or edit existing controller devices to cluster configuration. -options: - wait_until_configured_seconds: - description: - - How much time (in seconds) to wait for the device to connect to cluster post configuration. - type: int - default: 0 - vmanage_id: - description: - - Optional ID of vManage to edit. Don't set when adding new vManage instances to cluster. - type: str - system_ip: - description: - - Device system IP address. - type: str - cluster_ip: - description: - - Added/edited device cluster IP address. - type: str - username: - description: - - Username for the device being managed. - type: str - password: - description: - - Password for the device being managed. - type: str - no_log: True - gen_csr: - description: - - Whether to generate a CSR (Certificate Signing Request) for the device. - type: bool - persona: - description: - - Persona of the device. Choices are 'COMPUTE_AND_DATA', 'COMPUTE', or 'DATA'. - type: str - choices: ["COMPUTE_AND_DATA", "COMPUTE", "DATA"] - services: - description: - - A dict containing the services of cluster device, - such as Cisco Software-Defined Application Visibility and Control. - type: dict - tenancy: - description: - - Dictionary to configure tenancy settings. - type: dict - required: false - suboptions: - mode: - description: - - Tenancy mode for the cluster. - choices: ['single', 'multi'] - type: str - clusterid: - description: - - Unique identifier for the cluster in tenancy context. - type: str - domain: - description: - - Domain name associated with the tenancy. - type: str - -author: - - Przemyslaw Susko (sprzemys@cisco.com) -extends_documentation_fragment: - - cisco.catalystwan.manager_authentication -""" - -RETURN = r""" -msg: - description: Message detailing the outcome of the operation. - returned: always - type: str - sample: "Successfully updated requested vManage configuration." -response: - description: Detailed response from the vManage API if applicable. - returned: when API call is made - type: dict - sample: {"edit_vmanage": "successMessage": "Edit Node operation performed. The operation may take some time and - may cause application-server to restart in between"} -changed: - description: Whether or not the state was changed. - returned: always - type: bool - sample: true -""" - -EXAMPLES = r""" -# Example of using the module to edit parameters of vManage added to cluster -- name: "Edit vManage" - cisco.catalystwan.cluster_management: - wait_until_configured_seconds: 300 - vmanage_id: "0" - system_ip: "100.100.100.100" - cluster_ip: "1.1.1.1" - username: "username" - password: "password" # pragma: allowlist secret - persona: "COMPUTE_AND_DATA" - services: - sd-avc: - server: false - -# Example of using the module to add a new vManage to cluster -- name: "Add vManage to cluster" - cisco.catalystwan.cluster_management: - wait_until_configured_seconds: 300 - system_ip: "100.100.100.100" - cluster_ip: "2.2.2.2" - username: "username" - password: "password" # pragma: allowlist secret - gen_csr: false - persona: "DATA" - services: - sd-avc: - server: false - -- name: Create a cluster with tenancy configuration - cluster_management: - tenancy: - mode: multi - domain: "domain" -""" - -import time -from typing import List, Optional - -from catalystwan.endpoints.cluster_management import ConnectedDevice, TenancyMode, VManageSetup -from catalystwan.exceptions import ManagerRequestException - -from ..module_utils.result import ModuleResult -from ..module_utils.vmanage_module import AnsibleCatalystwanModule - - -def get_connected_devices(module, device_ip): - result = ModuleResult() - module.send_request_safely( - result, - action_name=f"Get connected devices for {device_ip}", - send_func=module.session.endpoints.cluster_management.get_connected_devices, - vmanageIP=device_ip, - response_key="connected_devices", - fail_on_exception=False, - ) - try: - return result.response["connected_devices"] - except KeyError: - return None - - -def is_device_connected_to_cluster(module, system_ip, cluster_ip): - connected_devices: List[ConnectedDevice] = get_connected_devices(module, cluster_ip) - for device in connected_devices: - if device["device_id"] == system_ip: - return True - return False - - -def wait_for_connected_device(module, system_ip, cluster_ip, timeout) -> Optional[str]: - start = time.time() - while True: - try: - if is_device_connected_to_cluster(module, system_ip, cluster_ip): - return None - if (time.time() - start) > timeout: - return f"reached timeout of {timeout}s" - time.sleep(1) - except ManagerRequestException: - time.sleep(1) - continue - return "unknown exception occurred" - - -def run_module(): - module_args = dict( - wait_until_configured_seconds=dict(type="int", default=0), - vmanage_id=dict(type=str), - system_ip=dict(type=str), - cluster_ip=dict(type=str), - username=dict(type=str), - password=dict(type=str, no_log=True), - gen_csr=dict(type=bool, aliases=["genCSR"]), - persona=dict(type=str, choices=["COMPUTE_AND_DATA", "COMPUTE", "DATA"]), - services=dict( - type="dict", - options=dict( - sd_avc=dict( - type="dict", - aliases=["sd-avc"], - options=dict( - server=dict(type="bool"), - ), - ), - ), - ), - tenancy=dict( - type="dict", - options=dict( - mode=dict(type="str", choices=["single", "multi"]), - clusterid=dict(type="str"), - domain=dict(type="str"), - ), - required=False, - ), - ) - - required_together = [("system_ip", "cluster_ip", "username", "password", "persona")] - - mutually_exclusive = [("tenancy", ("system_ip", "cluster_ip", "username", "password", "persona"))] - - required_one_of = [("tenancy", "system_ip", "cluster_ip", "username", "password", "persona")] - - module = AnsibleCatalystwanModule( - argument_spec=module_args, - session_reconnect_retries=180, - required_together=required_together, - mutually_exclusive=mutually_exclusive, - required_one_of=required_one_of, - ) - module.session.request_timeout = 60 - result = ModuleResult() - - if module.params.get("cluster_ip"): - vmanage_id = module.params.get("vmanage_id") - system_ip = module.params.get("system_ip") - cluster_ip = module.params.get("cluster_ip") - - if is_device_connected_to_cluster(module, system_ip, cluster_ip): - result.changed = False - result.msg = f"Device {cluster_ip} already configured" - module.exit_json(**result.model_dump(mode="json")) - - payload = VManageSetup( - vmanage_id=vmanage_id, - device_ip=cluster_ip, - username=module.params.get("username"), - password=module.params.get("password"), - persona=module.params.get("persona"), - services=module.params.get("services"), - ) - - if vmanage_id: - module.send_request_safely( - result, - action_name="Cluster Management: Edit vManage", - send_func=module.session.endpoints.cluster_management.edit_vmanage, - payload=payload, - response_key="edit_vmanage", - ) - else: - module.send_request_safely( - result, - action_name="Cluster Management: Add vManage", - send_func=module.session.endpoints.cluster_management.add_vmanage, - payload=payload, - response_key="add_vmanage", - ) - - if result.changed: - wait_until_configured_seconds = module.params.get("wait_until_configured_seconds") - if wait_until_configured_seconds: - error_msg = wait_for_connected_device(module, system_ip, cluster_ip, wait_until_configured_seconds) - if error_msg: - module.fail_json(msg=f"Error during vManage configuration: {error_msg}") - result.msg = "Successfully updated requested vManage configuration." - else: - result.msg = "No changes to vManage configuration applied." - - if module.params.get("tenancy"): - tenancy_mode: TenancyMode = module.get_response_safely( - module.session.endpoints.cluster_management.get_tenancy_mode - ) - - if tenancy_mode.mode == "MultiTenant" and module.params.get("tenancy").get("mode") != "multi": - module.fail_json(msg="Switching from MultiTenancy to SingleTenancy is forbidden") - - elif tenancy_mode.mode == "SingleTenant" and module.params.get("tenancy").get("mode") != "single": - new_tennacy = TenancyMode( - mode="MultiTenant", - domain=module.params.get("tenancy").get("domain"), - clusterid=module.params.get("tenancy").get("clusterid"), - ) - - module.send_request_safely( - result, - action_name="Cluster Management: Tenancy Mode", - send_func=module.session.endpoints.cluster_management.set_tenancy_mode, - payload=new_tennacy, - response_key=tenancy_mode, - ) - - if result.changed: - wait_until_configured_seconds = module.params.get("wait_until_configured_seconds") - if wait_until_configured_seconds: - # wait for manager to restart - time.sleep(60) - module.session.wait_server_ready(timeout=(wait_until_configured_seconds)) - result.msg = "Successfully updated requested vManage configuration. vManage was restarted" - else: - result.msg = "Successfully updated requested vManage configuration. vManage will be restarted" - else: - result.msg = "No changes to vManage configuration applied." - - module.exit_json(**result.model_dump(mode="json")) - - -def main(): - run_module() - - -if __name__ == "__main__": - main() +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = r""" +--- +module: cluster_management +short_description: Cluster configuration for vManage devices +version_added: "0.2.1" +description: This module can be used to add or edit existing controller devices to cluster configuration. +options: + wait_until_configured_seconds: + description: + - How much time (in seconds) to wait for the device to connect to cluster post configuration. + type: int + default: 0 + vmanage_id: + description: + - Optional ID of vManage to edit. Don't set when adding new vManage instances to cluster. + type: str + system_ip: + description: + - Device system IP address. + type: str + cluster_ip: + description: + - Added/edited device cluster IP address. + type: str + username: + description: + - Username for the device being managed. + type: str + password: + description: + - Password for the device being managed. + type: str + gen_csr: + description: + - Whether to generate a CSR (Certificate Signing Request) for the device. + type: bool + persona: + description: + - Persona of the device. Choices are 'COMPUTE_AND_DATA', 'COMPUTE', or 'DATA'. + type: str + choices: ["COMPUTE_AND_DATA", "COMPUTE", "DATA"] + services: + description: + - A dict containing the services of cluster device, + such as Cisco Software-Defined Application Visibility and Control. + type: dict + tenancy: + description: + - Dictionary to configure tenancy settings. + type: dict + required: false + suboptions: + mode: + description: + - Tenancy mode for the cluster. + choices: ['single', 'multi'] + type: str + clusterid: + description: + - Unique identifier for the cluster in tenancy context. + type: str + domain: + description: + - Domain name associated with the tenancy. + type: str + +author: + - Przemyslaw Susko (@sprzemys) +extends_documentation_fragment: + - cisco.catalystwan.manager_authentication +""" + +RETURN = r""" +msg: + description: Message detailing the outcome of the operation. + returned: always + type: str + sample: "Successfully updated requested vManage configuration." +response: + description: Detailed response from the vManage API if applicable. + returned: when API call is made + type: dict + sample: + edit_vmanage: + successMessage: >- + Edit Node operation performed. The operation may take some time and may + cause application-server to restart in between. +changed: + description: Whether or not the state was changed. + returned: always + type: bool + sample: true +""" + +EXAMPLES = r""" +# Example of using the module to edit parameters of vManage added to cluster +- name: "Edit vManage" + cisco.catalystwan.cluster_management: + wait_until_configured_seconds: 300 + vmanage_id: "0" + system_ip: "100.100.100.100" + cluster_ip: "1.1.1.1" + username: "username" + password: "password" # pragma: allowlist secret + persona: "COMPUTE_AND_DATA" + services: + sd-avc: + server: false + +# Example of using the module to add a new vManage to cluster +- name: "Add vManage to cluster" + cisco.catalystwan.cluster_management: + wait_until_configured_seconds: 300 + system_ip: "100.100.100.100" + cluster_ip: "2.2.2.2" + username: "username" + password: "password" # pragma: allowlist secret + gen_csr: false + persona: "DATA" + services: + sd-avc: + server: false + +- name: Create a cluster with tenancy configuration + cluster_management: + tenancy: + mode: multi + domain: "domain" +""" + +import time +from typing import Dict, List, Optional + +from catalystwan.endpoints.cluster_management import TenancyMode +from catalystwan.exceptions import ManagerRequestException +from pydantic import BaseModel, ConfigDict, Field + +try: + from catalystwan.endpoints.cluster_management import ConnectedDevice, VManageSetup + + HAS_CLUSTER_MANAGEMENT_ENDPOINTS = True +except ImportError: + HAS_CLUSTER_MANAGEMENT_ENDPOINTS = False + + class ConnectedDevice(BaseModel): + model_config = ConfigDict(populate_by_name=True) + uuid: str + device_id: str = Field(serialization_alias="deviceId", validation_alias="deviceId") + + class VManageSetup(BaseModel): + model_config = ConfigDict(populate_by_name=True) + vmanage_id: Optional[str] = Field(default=None, serialization_alias="vmanageID", validation_alias="vmanageID") + device_ip: str = Field(serialization_alias="deviceIP", validation_alias="deviceIP") + username: str + password: str + gen_csr: Optional[bool] = Field(default=None, serialization_alias="genCSR", validation_alias="genCSR") + persona: str + services: Optional[Dict[str, Dict[str, bool]]] = None + + +from ..module_utils.result import ModuleResult +from ..module_utils.vmanage_module import AnsibleCatalystwanModule + + +def get_connected_devices(module, device_ip): + result = ModuleResult() + module.send_request_safely( + result, + action_name=f"Get connected devices for {device_ip}", + send_func=module.session.endpoints.cluster_management.get_connected_devices, + vmanageIP=device_ip, + response_key="connected_devices", + fail_on_exception=False, + ) + try: + return result.response["connected_devices"] + except KeyError: + return None + + +def is_device_connected_to_cluster(module, system_ip, cluster_ip): + connected_devices: List[ConnectedDevice] = get_connected_devices(module, cluster_ip) + for device in connected_devices: + if device["device_id"] == system_ip: + return True + return False + + +def wait_for_connected_device(module, system_ip, cluster_ip, timeout) -> Optional[str]: + start = time.time() + while True: + try: + if is_device_connected_to_cluster(module, system_ip, cluster_ip): + return None + if (time.time() - start) > timeout: + return f"reached timeout of {timeout}s" + time.sleep(1) + except ManagerRequestException: + time.sleep(1) + continue + return "unknown exception occurred" + + +def run_module(): + module_args = dict( + wait_until_configured_seconds=dict(type="int", default=0), + vmanage_id=dict(type=str), + system_ip=dict(type=str), + cluster_ip=dict(type=str), + username=dict(type=str), + password=dict(type=str, no_log=True), + gen_csr=dict(type=bool, aliases=["genCSR"]), + persona=dict(type=str, choices=["COMPUTE_AND_DATA", "COMPUTE", "DATA"]), + services=dict( + type="dict", + options=dict( + sd_avc=dict( + type="dict", + aliases=["sd-avc"], + options=dict( + server=dict(type="bool"), + ), + ), + ), + ), + tenancy=dict( + type="dict", + options=dict( + mode=dict(type="str", choices=["single", "multi"]), + clusterid=dict(type="str"), + domain=dict(type="str"), + ), + required=False, + ), + ) + + required_together = [("system_ip", "cluster_ip", "username", "password", "persona")] + + mutually_exclusive = [("tenancy", ("system_ip", "cluster_ip", "username", "password", "persona"))] + + required_one_of = [("tenancy", "system_ip", "cluster_ip", "username", "password", "persona")] + + module = AnsibleCatalystwanModule( + argument_spec=module_args, + session_reconnect_retries=180, + required_together=required_together, + mutually_exclusive=mutually_exclusive, + required_one_of=required_one_of, + ) + if not HAS_CLUSTER_MANAGEMENT_ENDPOINTS: + module.fail_json( + msg=( + "The installed catalystwan SDK no longer exposes the cluster-management write endpoints. " + "Install catalystwan==0.41.5.dev2 to use this module." + ) + ) + module.session.request_timeout = 60 + result = ModuleResult() + + if module.params.get("cluster_ip"): + vmanage_id = module.params.get("vmanage_id") + system_ip = module.params.get("system_ip") + cluster_ip = module.params.get("cluster_ip") + + if is_device_connected_to_cluster(module, system_ip, cluster_ip): + result.changed = False + result.msg = f"Device {cluster_ip} already configured" + module.exit_json(**result.model_dump(mode="json")) + + payload = VManageSetup( + vmanage_id=vmanage_id, + device_ip=cluster_ip, + username=module.params.get("username"), + password=module.params.get("password"), + persona=module.params.get("persona"), + services=module.params.get("services"), + ) + + if vmanage_id: + module.send_request_safely( + result, + action_name="Cluster Management: Edit vManage", + send_func=module.session.endpoints.cluster_management.edit_vmanage, + payload=payload, + response_key="edit_vmanage", + ) + else: + module.send_request_safely( + result, + action_name="Cluster Management: Add vManage", + send_func=module.session.endpoints.cluster_management.add_vmanage, + payload=payload, + response_key="add_vmanage", + ) + + if result.changed: + wait_until_configured_seconds = module.params.get("wait_until_configured_seconds") + if wait_until_configured_seconds: + error_msg = wait_for_connected_device(module, system_ip, cluster_ip, wait_until_configured_seconds) + if error_msg: + module.fail_json(msg=f"Error during vManage configuration: {error_msg}") + result.msg = "Successfully updated requested vManage configuration." + else: + result.msg = "No changes to vManage configuration applied." + + if module.params.get("tenancy"): + tenancy_mode: TenancyMode = module.get_response_safely( + module.session.endpoints.cluster_management.get_tenancy_mode + ) + + if tenancy_mode.mode == "MultiTenant" and module.params.get("tenancy").get("mode") != "multi": + module.fail_json(msg="Switching from MultiTenancy to SingleTenancy is forbidden") + + elif tenancy_mode.mode == "SingleTenant" and module.params.get("tenancy").get("mode") != "single": + new_tennacy = TenancyMode( + mode="MultiTenant", + domain=module.params.get("tenancy").get("domain"), + clusterid=module.params.get("tenancy").get("clusterid"), + ) + + module.send_request_safely( + result, + action_name="Cluster Management: Tenancy Mode", + send_func=module.session.endpoints.cluster_management.set_tenancy_mode, + payload=new_tennacy, + response_key=tenancy_mode, + ) + + if result.changed: + wait_until_configured_seconds = module.params.get("wait_until_configured_seconds") + if wait_until_configured_seconds: + # wait for manager to restart + time.sleep(60) + module.session.wait_server_ready(timeout=(wait_until_configured_seconds)) + result.msg = "Successfully updated requested vManage configuration. vManage was restarted" + else: + result.msg = "Successfully updated requested vManage configuration. vManage will be restarted" + else: + result.msg = "No changes to vManage configuration applied." + + module.exit_json(**result.model_dump(mode="json")) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/config_group_deployment.py b/plugins/modules/config_group_deployment.py index 40eb46e..ba9c90b 100644 --- a/plugins/modules/config_group_deployment.py +++ b/plugins/modules/config_group_deployment.py @@ -1,137 +1,137 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright 2024 Cisco Systems, Inc. and its affiliates -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - -DOCUMENTATION = r""" ---- -module: config_group_deployment -short_description: Description -version_added: "0.3.1" -description: Module for deployment of config groups. -author: - - Przemyslaw Susko (sprzemys@cisco.com) -extends_documentation_fragment: - - cisco.catalystwan.manager_authentication -""" - -RETURN = r""" -msg: - description: Message detailing the outcome of the operation. - returned: always - type: str -response: - description: Detailed response from the vManage API if applicable. - returned: when API call is made - type: dict -changed: - description: Whether or not the state was changed. - returned: always - type: bool - sample: true -""" - -EXAMPLES = r""" -- name: "Deploy config group" - cisco.catalystwan.config_group_deployment: - config_group_id: c90cdc29-fbc7-470a-80ad-6c81beb35848 - edge_device_variables: - - admin_password: password - hostname: cedge-1 - pseudo_commit_timer: 300 - site_id: '1001' - system_ip: 192.168.101.1 - uuid: XXX-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX1 - vpn_0_transport_if: GigabitEthernet1 - vpn_10_if_0: GigabitEthernet2 - vpn_10_if_0_static_ipaddr: 10.0.0.1 - vpn_10_if_0_static_subnet: 255.255.255.0 - - admin_password: Cisco#!@#@ViptelaxDD - hostname: sprzemys-cedge-2 - pseudo_commit_timer: 300 - site_id: '1002' - system_ip: 192.168.102.1 - uuid: XXX-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX2 - vpn_0_transport_if: GigabitEthernet1 - vpn_10_if_0: GigabitEthernet2 - vpn_10_if_0_static_ipaddr: 10.0.0.2 - vpn_10_if_0_static_subnet: 255.255.255.0 -""" - -import traceback - -from catalystwan.endpoints.configuration_group import DeviceVariables, VariableData - -from ..module_utils.result import ModuleResult -from ..module_utils.vmanage_module import AnsibleCatalystwanModule - - -def normalize_variables(variables: dict): - return_variables = {} - key_map = {"hostname": "host_name"} - - for key, value in variables.items(): - new_key = key_map.get(key, key) - if new_key == "site_id" and isinstance(value, str): - value = int(value) - return_variables[new_key] = value - - return return_variables - - -def normalize_variables_list(variables: list): - return_variables = [] - for v in variables: - return_variables.append(normalize_variables(v)) - return return_variables - - -def generate_payload_for_device(module: AnsibleCatalystwanModule, source: dict, device_ids: list): - try: - variables = [] - for key, value in source.items(): - if key == "uuid": - continue - variables.append(VariableData(name=key, value=value)) - - device_ids.append(source["uuid"]) - return DeviceVariables(device_id=source["uuid"], variables=variables) - except Exception as exception: - module.fail_json(msg=f"Unknown exception: {exception}", exception=traceback.format_exc()) - - -def run_module(): - module_args = dict( - config_group_id=dict(type=str, required=True), - edge_device_variables=dict(type=list, required=True), - ) - - module = AnsibleCatalystwanModule(argument_spec=module_args) - result = ModuleResult() - - config_group_id = module.params.get("config_group_id") - edge_device_variables = normalize_variables_list(module.params.get("edge_device_variables")) - - device_ids = [] - variables_payload = [] - for device in edge_device_variables: - variables_payload.append(generate_payload_for_device(module, device, device_ids)) - - try: - module.session.api.config_group.associate(config_group_id, device_ids) - module.session.api.config_group.update_variables(config_group_id, "sdwan", variables_payload) - response = module.session.api.config_group.deploy(config_group_id, device_ids) - result.response = response - result.changed = True - module.exit_json(**result.model_dump(mode="json")) - except Exception as exception: - module.fail_json(msg=f"Unknown exception: {exception}", exception=traceback.format_exc()) - - -def main(): - run_module() - - -if __name__ == "__main__": - main() +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: config_group_deployment +short_description: Description +version_added: "0.3.1" +description: Module for deployment of config groups. +author: + - Przemyslaw Susko (@sprzemys) +extends_documentation_fragment: + - cisco.catalystwan.manager_authentication +""" + +RETURN = r""" +msg: + description: Message detailing the outcome of the operation. + returned: always + type: str +response: + description: Detailed response from the vManage API if applicable. + returned: when API call is made + type: dict +changed: + description: Whether or not the state was changed. + returned: always + type: bool + sample: true +""" + +EXAMPLES = r""" +- name: "Deploy config group" + cisco.catalystwan.config_group_deployment: + config_group_id: c90cdc29-fbc7-470a-80ad-6c81beb35848 + edge_device_variables: + - admin_password: password + hostname: cedge-1 + pseudo_commit_timer: 300 + site_id: '1001' + system_ip: 192.168.101.1 + uuid: XXX-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX1 + vpn_0_transport_if: GigabitEthernet1 + vpn_10_if_0: GigabitEthernet2 + vpn_10_if_0_static_ipaddr: 10.0.0.1 + vpn_10_if_0_static_subnet: 255.255.255.0 + - admin_password: Cisco#!@#@ViptelaxDD + hostname: sprzemys-cedge-2 + pseudo_commit_timer: 300 + site_id: '1002' + system_ip: 192.168.102.1 + uuid: XXX-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX2 + vpn_0_transport_if: GigabitEthernet1 + vpn_10_if_0: GigabitEthernet2 + vpn_10_if_0_static_ipaddr: 10.0.0.2 + vpn_10_if_0_static_subnet: 255.255.255.0 +""" + +import traceback + +from catalystwan.endpoints.configuration_group import DeviceVariables, VariableData + +from ..module_utils.result import ModuleResult +from ..module_utils.vmanage_module import AnsibleCatalystwanModule + + +def normalize_variables(variables: dict): + return_variables = {} + key_map = {"hostname": "host_name"} + + for key, value in variables.items(): + new_key = key_map.get(key, key) + if new_key == "site_id" and isinstance(value, str): + value = int(value) + return_variables[new_key] = value + + return return_variables + + +def normalize_variables_list(variables: list): + return_variables = [] + for v in variables: + return_variables.append(normalize_variables(v)) + return return_variables + + +def generate_payload_for_device(module: AnsibleCatalystwanModule, source: dict, device_ids: list): + try: + variables = [] + for key, value in source.items(): + if key == "uuid": + continue + variables.append(VariableData(name=key, value=value)) + + device_ids.append(source["uuid"]) + return DeviceVariables(device_id=source["uuid"], variables=variables) + except Exception as exception: + module.fail_json(msg=f"Unknown exception: {exception}", exception=traceback.format_exc()) + + +def run_module(): + module_args = dict( + config_group_id=dict(type=str, required=True), + edge_device_variables=dict(type=list, required=True), + ) + + module = AnsibleCatalystwanModule(argument_spec=module_args) + result = ModuleResult() + + config_group_id = module.params.get("config_group_id") + edge_device_variables = normalize_variables_list(module.params.get("edge_device_variables")) + + device_ids = [] + variables_payload = [] + for device in edge_device_variables: + variables_payload.append(generate_payload_for_device(module, device, device_ids)) + + try: + module.session.api.config_group.associate(config_group_id, device_ids) + module.session.api.config_group.update_variables(config_group_id, "sdwan", variables_payload) + response = module.session.api.config_group.deploy(config_group_id, device_ids) + result.response = response + result.changed = True + module.exit_json(**result.model_dump(mode="json")) + except Exception as exception: + module.fail_json(msg=f"Unknown exception: {exception}", exception=traceback.format_exc()) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/config_groups.py b/plugins/modules/config_groups.py index 2bcc15b..1d2af75 100644 --- a/plugins/modules/config_groups.py +++ b/plugins/modules/config_groups.py @@ -1,212 +1,233 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright 2024 Cisco Systems, Inc. and its affiliates -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - -DOCUMENTATION = r""" ---- -module: config_groups -short_description: Description -version_added: "0.3.1" -description: Module for configuration of config groups. -author: - - Przemyslaw Susko (sprzemys@cisco.com) -extends_documentation_fragment: - - cisco.catalystwan.manager_authentication -""" - -RETURN = r""" -msg: - description: Message detailing the outcome of the operation. - returned: always - type: str -response: - description: Detailed response from the vManage API if applicable. - returned: when API call is made - type: dict -changed: - description: Whether or not the state was changed. - returned: always - type: bool - sample: true -id: - description: ID of created config group. - returned: on success - type: bool - sample: c90cdc29-fbc7-470a-80ad-6c81beb35848 -""" - -EXAMPLES = r""" -- name: "Create config group" - cisco.catalystwan.config_groups: - name: NAME - description: DESCRIPTION - system_profiles: - - name: "{{ config_group_name }}_Basic" - description: "{{ config_group_name }} Basic Profile" - parcels: - - type: banner - config: - name: Banner - description: Banner Description -""" - -import traceback - -from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload -from catalystwan.models.configuration.feature_profile.sdwan.service import ( - InterfaceEthernetParcel as ServiceInterfaceEthernetParcel, -) -from catalystwan.models.configuration.feature_profile.sdwan.service import LanVpnParcel -from catalystwan.models.configuration.feature_profile.sdwan.system import ( - AAAParcel, - BannerParcel, - BasicParcel, - BFDParcel, - GlobalParcel, - LoggingParcel, - MRFParcel, - NtpParcel, - OMPParcel, -) -from catalystwan.models.configuration.feature_profile.sdwan.transport import ( - InterfaceEthernetParcel as TransportInterfaceEthernetParcel, -) -from catalystwan.models.configuration.feature_profile.sdwan.transport import TransportVpnParcel - -from ..module_utils.result import ModuleResult -from ..module_utils.vmanage_module import AnsibleCatalystwanModule - -system_parcel_type_mapping = { - "banner": BannerParcel, - "basic": BasicParcel, - "bfd": BFDParcel, - "omp": OMPParcel, - "logging": LoggingParcel, - "ntp": NtpParcel, - "global": GlobalParcel, - "aaa": AAAParcel, - "mrf": MRFParcel, -} - -transport_parcel_type_mapping = { - "vpn": TransportVpnParcel, - "ethernet": TransportInterfaceEthernetParcel, -} - -service_parcel_type_mapping = { - "vpn": LanVpnParcel, - "ethernet": ServiceInterfaceEthernetParcel, -} - - -def create_parcel(module, parcel, profile_type): - if "config" not in parcel: - module.fail_json(msg="{} parcel lacks config".format(parcel["type"])) - - parcel_type = parcel["type"] - if profile_type == "system": - parcel_class = system_parcel_type_mapping.get(parcel_type) - elif profile_type == "transport": - parcel_class = transport_parcel_type_mapping.get(parcel_type) - elif profile_type == "service": - parcel_class = service_parcel_type_mapping.get(parcel_type) - else: - raise ValueError(f"Unknown profile type {profile_type}") - - if parcel_class: - try: - return parcel_class(**parcel["config"]) - except Exception as ex: - module.fail_json( - "Failed to parse {} type parcel for {} profile. Exception: {}".format(parcel["type"], profile_type, ex) - ) - else: - module.fail_json(msg=f"Unknown parcel type: {parcel_type}") - - -def builder_add_parcel_vpn(builder, module, parcel, profile_type): - vpn_tag = builder.add_parcel_vpn(create_parcel(module, parcel, profile_type)) - if "sub_parcels" in parcel: - for sub_parcel in parcel["sub_parcels"]: - if profile_type == "transport": - builder.add_vpn_subparcel(vpn_tag, create_parcel(module, sub_parcel, profile_type)) - elif profile_type == "service": - builder.add_parcel_vpn_subparcel(vpn_tag, create_parcel(module, sub_parcel, profile_type)) - - -def create_profile(module, profile, profile_type): - if "name" not in profile: - module.fail_json(msg=f"{profile_type} profile lacks name") - if "parcels" not in profile: - module.fail_json(msg="{} profile {} lacks parcels".format(profile_type, profile["name"])) - - builder = module.session.api.builders.feature_profiles.create_builder(profile_type) - builder.add_profile_name_and_description( - FeatureProfileCreationPayload( - name=profile["name"], description=profile["description"] if "description" in profile else "" - ) - ) - for parcel in profile["parcels"]: - if "type" not in parcel: - module.fail_json(msg="parcel for profile {} lacks type".format(profile["name"])) - - if parcel["type"] == "vpn": - builder_add_parcel_vpn(builder, module, parcel, profile_type) - else: - builder.add_parcel(create_parcel(module, parcel, profile_type)) - - build_report = builder.build() - if len(build_report.failed_parcels) > 0: - module.fail_json( - msg="Failed to create {} parcels for profile {}." - "Build report: {}".format(len(build_report.failed_parcels), profile["name"], build_report) - ) - - return build_report.profile_uuid - - -def run_module(): - module_args = dict( - name=dict(type=str, required=True), - description=dict(type=str), - system_profiles=dict(type=list), - transport_profiles=dict(type=list), - service_profiles=dict(type=list), - ) - - module = AnsibleCatalystwanModule(argument_spec=module_args) - result = ModuleResult() - profile_ids = [] - - name = module.params.get("name") - description = module.params.get("description") - system_profiles = module.params.get("system_profiles") - transport_profiles = module.params.get("transport_profiles") - service_profiles = module.params.get("service_profiles") - - for profile in system_profiles: - profile_ids.append(create_profile(module, profile, "system")) - for profile in transport_profiles: - profile_ids.append(create_profile(module, profile, "transport")) - for profile in service_profiles: - profile_ids.append(create_profile(module, profile, "service")) - - try: - response = module.session.api.config_group.create(name, description, "sdwan", profile_ids) - result.response = response - result.changed = True - result.id = response.id - - module.exit_json(**result.model_dump(mode="json")) - except Exception as exception: - module.fail_json(msg=f"Unknown exception: {exception}", exception=traceback.format_exc()) - - -def main(): - run_module() - - -if __name__ == "__main__": - main() +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: config_groups +short_description: Description +version_added: "0.3.1" +description: Module for configuration of config groups. +author: + - Przemyslaw Susko (@sprzemys) +extends_documentation_fragment: + - cisco.catalystwan.manager_authentication +""" + +RETURN = r""" +msg: + description: Message detailing the outcome of the operation. + returned: always + type: str +response: + description: Detailed response from the vManage API if applicable. + returned: when API call is made + type: dict +changed: + description: Whether or not the state was changed. + returned: always + type: bool + sample: true +id: + description: ID of created config group. + returned: on success + type: bool + sample: c90cdc29-fbc7-470a-80ad-6c81beb35848 +""" + +EXAMPLES = r""" +- name: "Create config group" + cisco.catalystwan.config_groups: + name: NAME + description: DESCRIPTION + system_profiles: + - name: "{{ config_group_name }}_Basic" + description: "{{ config_group_name }} Basic Profile" + parcels: + - type: banner + config: + name: Banner + description: Banner Description +""" + +import traceback + +from catalystwan.models.configuration.feature_profile.common import FeatureProfileCreationPayload + +try: + from catalystwan.models.configuration.feature_profile.sdwan.service import ( + InterfaceEthernetParcel as ServiceInterfaceEthernetParcel, + ) + from catalystwan.models.configuration.feature_profile.sdwan.service import ( + LanVpnParcel, + ) + from catalystwan.models.configuration.feature_profile.sdwan.system import ( + AAAParcel, + BannerParcel, + BasicParcel, + BFDParcel, + GlobalParcel, + LoggingParcel, + MRFParcel, + NtpParcel, + OMPParcel, + ) + from catalystwan.models.configuration.feature_profile.sdwan.transport import ( + InterfaceEthernetParcel as TransportInterfaceEthernetParcel, + ) + from catalystwan.models.configuration.feature_profile.sdwan.transport import ( + TransportVpnParcel, + ) + + HAS_LEGACY_FEATURE_PROFILE_BUILDER = True +except ImportError: + HAS_LEGACY_FEATURE_PROFILE_BUILDER = False + +from ..module_utils.result import ModuleResult +from ..module_utils.vmanage_module import AnsibleCatalystwanModule + +system_parcel_type_mapping = ( + { + "banner": BannerParcel, + "basic": BasicParcel, + "bfd": BFDParcel, + "omp": OMPParcel, + "logging": LoggingParcel, + "ntp": NtpParcel, + "global": GlobalParcel, + "aaa": AAAParcel, + "mrf": MRFParcel, + } + if HAS_LEGACY_FEATURE_PROFILE_BUILDER + else {} +) + +transport_parcel_type_mapping = ( + {"vpn": TransportVpnParcel, "ethernet": TransportInterfaceEthernetParcel} + if HAS_LEGACY_FEATURE_PROFILE_BUILDER + else {} +) + +service_parcel_type_mapping = ( + {"vpn": LanVpnParcel, "ethernet": ServiceInterfaceEthernetParcel} if HAS_LEGACY_FEATURE_PROFILE_BUILDER else {} +) + + +def create_parcel(module, parcel, profile_type): + if "config" not in parcel: + module.fail_json(msg="{} parcel lacks config".format(parcel["type"])) + + parcel_type = parcel["type"] + if profile_type == "system": + parcel_class = system_parcel_type_mapping.get(parcel_type) + elif profile_type == "transport": + parcel_class = transport_parcel_type_mapping.get(parcel_type) + elif profile_type == "service": + parcel_class = service_parcel_type_mapping.get(parcel_type) + else: + raise ValueError(f"Unknown profile type {profile_type}") + + if parcel_class: + try: + return parcel_class(**parcel["config"]) + except Exception as ex: + module.fail_json( + "Failed to parse {} type parcel for {} profile. Exception: {}".format(parcel["type"], profile_type, ex) + ) + else: + module.fail_json(msg=f"Unknown parcel type: {parcel_type}") + + +def builder_add_parcel_vpn(builder, module, parcel, profile_type): + vpn_tag = builder.add_parcel_vpn(create_parcel(module, parcel, profile_type)) + if "sub_parcels" in parcel: + for sub_parcel in parcel["sub_parcels"]: + if profile_type == "transport": + builder.add_vpn_subparcel(vpn_tag, create_parcel(module, sub_parcel, profile_type)) + elif profile_type == "service": + builder.add_parcel_vpn_subparcel(vpn_tag, create_parcel(module, sub_parcel, profile_type)) + + +def create_profile(module, profile, profile_type): + if "name" not in profile: + module.fail_json(msg=f"{profile_type} profile lacks name") + if "parcels" not in profile: + module.fail_json(msg="{} profile {} lacks parcels".format(profile_type, profile["name"])) + + builder = module.session.api.builders.feature_profiles.create_builder(profile_type) + builder.add_profile_name_and_description( + FeatureProfileCreationPayload( + name=profile["name"], description=profile["description"] if "description" in profile else "" + ) + ) + for parcel in profile["parcels"]: + if "type" not in parcel: + module.fail_json(msg="parcel for profile {} lacks type".format(profile["name"])) + + if parcel["type"] == "vpn": + builder_add_parcel_vpn(builder, module, parcel, profile_type) + else: + builder.add_parcel(create_parcel(module, parcel, profile_type)) + + build_report = builder.build() + if len(build_report.failed_parcels) > 0: + module.fail_json( + msg="Failed to create {} parcels for profile {}." + "Build report: {}".format(len(build_report.failed_parcels), profile["name"], build_report) + ) + + return build_report.profile_uuid + + +def run_module(): + module_args = dict( + name=dict(type=str, required=True), + description=dict(type=str), + system_profiles=dict(type=list), + transport_profiles=dict(type=list), + service_profiles=dict(type=list), + ) + + module = AnsibleCatalystwanModule(argument_spec=module_args) + if not HAS_LEGACY_FEATURE_PROFILE_BUILDER: + module.fail_json( + msg=( + "The installed catalystwan SDK no longer exposes the legacy feature-profile builder. " + "Install catalystwan==0.41.5.dev2 to use this module." + ) + ) + result = ModuleResult() + profile_ids = [] + + name = module.params.get("name") + description = module.params.get("description") + system_profiles = module.params.get("system_profiles") + transport_profiles = module.params.get("transport_profiles") + service_profiles = module.params.get("service_profiles") + + for profile in system_profiles: + profile_ids.append(create_profile(module, profile, "system")) + for profile in transport_profiles: + profile_ids.append(create_profile(module, profile, "transport")) + for profile in service_profiles: + profile_ids.append(create_profile(module, profile, "service")) + + try: + response = module.session.api.config_group.create(name, description, "sdwan", profile_ids) + result.response = response + result.changed = True + result.id = response.id + + module.exit_json(**result.model_dump(mode="json")) + except Exception as exception: + module.fail_json(msg=f"Unknown exception: {exception}", exception=traceback.format_exc()) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/device_templates.py b/plugins/modules/device_templates.py index fba0f9d..27c3715 100644 --- a/plugins/modules/device_templates.py +++ b/plugins/modules/device_templates.py @@ -7,7 +7,7 @@ DOCUMENTATION = r""" --- -module: Device_templates +module: device_templates short_description: Manage Device Templates on vManage. version_added: "0.2.0" description: @@ -72,7 +72,7 @@ for these parameters. type: raw author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.device_models_device_template - cisco.catalystwan.manager_authentication @@ -133,11 +133,19 @@ from catalystwan.api.template_api import DeviceTemplate, GeneralTemplate from catalystwan.dataclasses import Device from catalystwan.exceptions import TemplateNotFoundError -from catalystwan.models.common import DeviceModel -from catalystwan.models.templates import DeviceTemplateInformation from catalystwan.session import ManagerHTTPError from catalystwan.typed_list import DataSequence +try: + from catalystwan.dataclasses import DeviceTemplateInfo +except ImportError: + from catalystwan.models.templates import DeviceTemplateInformation as DeviceTemplateInfo + +try: + from catalystwan.utils.device_model import DeviceModel +except ImportError: + from catalystwan.models.common import DeviceModel + from ..module_utils.result import ModuleResult from ..module_utils.vmanage_module import AnsibleCatalystwanModule @@ -205,10 +213,10 @@ def run_module(): template_name = module.params.get("template_name") - all_templates: DataSequence[DeviceTemplateInformation] = module.get_response_safely( + all_templates: DataSequence[DeviceTemplateInfo] = module.get_response_safely( module.session.api.templates.get, template=DeviceTemplate ) - target_template: Optional[DeviceTemplateInformation] = all_templates.filter(name=template_name) + target_template: Optional[DeviceTemplateInfo] = all_templates.filter(name=template_name) if module.params.get("state") == "present": # Code for checking if template name exists already diff --git a/plugins/modules/device_templates_info.py b/plugins/modules/device_templates_info.py index e13366f..0f59cc3 100644 --- a/plugins/modules/device_templates_info.py +++ b/plugins/modules/device_templates_info.py @@ -7,7 +7,7 @@ DOCUMENTATION = r""" --- -module: Device_templates_info +module: device_templates_info short_description: Get information about Device Templates on vManage. version_added: "0.2.0" description: @@ -118,7 +118,7 @@ - Directory to store the backup. It's created if missing. Defaults to a 'backup' folder in the current directory. type: path author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication notes: @@ -174,11 +174,15 @@ from typing import Dict, List, Optional from catalystwan.api.template_api import DeviceTemplate -from catalystwan.models.templates import DeviceTemplateInformation from catalystwan.session import ManagerHTTPError from catalystwan.typed_list import DataSequence from pydantic import BaseModel, Field +try: + from catalystwan.dataclasses import DeviceTemplateInfo +except ImportError: + from catalystwan.models.templates import DeviceTemplateInformation as DeviceTemplateInfo + from ..module_utils.result import ModuleResult from ..module_utils.vmanage_module import AnsibleCatalystwanModule @@ -190,8 +194,8 @@ class BackupPathModel(BaseModel): class ExtendedModuleResult(ModuleResult): - templates_info: Optional[Dict] = Field(default={}) - backup_paths: Optional[List[BackupPathModel]] = Field(default=[]) + templates_info: Optional[Dict] = Field(default_factory=dict) + backup_paths: Optional[List[BackupPathModel]] = Field(default_factory=list) def run_module(): @@ -205,9 +209,9 @@ def run_module(): module = AnsibleCatalystwanModule(argument_spec=module_args) filters = module.params.get("filters") - filtered_templates = DataSequence(DeviceTemplateInformation) + filtered_templates = DataSequence(DeviceTemplateInfo) - all_templates: DataSequence[DeviceTemplateInformation] = module.get_response_safely( + all_templates: DataSequence[DeviceTemplateInfo] = module.get_response_safely( module.session.api.templates.get, template=DeviceTemplate ) @@ -216,13 +220,13 @@ def run_module(): if filtered_templates: module.logger.info(f"All Device Templates filtered with filters: {filters}:\n{filtered_templates}") result.msg = "Succesfully got all requested Device Templates Info from vManage" - result.templates_info = [template for template in filtered_templates] + result.templates_info = list(filtered_templates) else: module.logger.warning(msg=f"Device templates filtered with `{filters}` not present.") result.msg = f"Device templates filtered with `{filters}` not present on vManage." else: result.msg = "Succesfully got all Device Templates Info from vManage" - result.templates_info = [template for template in all_templates] + result.templates_info = list(all_templates) if module.params.get("backup"): backup_dir_path: Path = Path(module.params.get("backup_dir_path")) diff --git a/plugins/modules/device_templates_recovery.py b/plugins/modules/device_templates_recovery.py index be5b78e..1549b58 100644 --- a/plugins/modules/device_templates_recovery.py +++ b/plugins/modules/device_templates_recovery.py @@ -122,7 +122,7 @@ default: null type: str author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication notes: @@ -138,7 +138,14 @@ from pathlib import Path, PurePath from catalystwan.session import ManagerHTTPError -from catalystwan.workflows import backup_restore_device_templates + +try: + from catalystwan.workflows import backup_restore_device_templates + + HAS_TEMPLATE_RECOVERY_WORKFLOW = True +except ImportError: + backup_restore_device_templates = None + HAS_TEMPLATE_RECOVERY_WORKFLOW = False from ..module_utils.result import ModuleResult from ..module_utils.vmanage_module import AnsibleCatalystwanModule @@ -157,6 +164,13 @@ def run_module(): result = ModuleResult() module = AnsibleCatalystwanModule(argument_spec=module_args) + if not HAS_TEMPLATE_RECOVERY_WORKFLOW: + module.fail_json( + msg=( + "The installed catalystwan SDK no longer includes the device-template recovery workflow. " + "Install catalystwan==0.41.5.dev2 to use this module." + ) + ) filters = module.params.get("filters") backup_dir_path: Path = Path(module.params.get("backup_dir_path")) diff --git a/plugins/modules/devices_certificates.py b/plugins/modules/devices_certificates.py index 0ec581c..7a4685f 100644 --- a/plugins/modules/devices_certificates.py +++ b/plugins/modules/devices_certificates.py @@ -72,7 +72,7 @@ - UUID of the device. type: str author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) notes: - Actions are mutually exclusive and only one action can be performed at a time. diff --git a/plugins/modules/devices_controllers.py b/plugins/modules/devices_controllers.py index 6868c4d..bb4885a 100644 --- a/plugins/modules/devices_controllers.py +++ b/plugins/modules/devices_controllers.py @@ -28,7 +28,6 @@ description: - Password for the device being managed. type: str - no_log: True personality: description: - Personality of the device. Choices are 'vSmart', 'vBond', or 'vManage'. @@ -61,7 +60,7 @@ - Hostname of the device. type: str author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) notes: - "Only vSmart, vBond, and vManage device personalities are currently supported." - "The 'state' option 'invalidated' will delete the device configuration in vManage." diff --git a/plugins/modules/devices_info.py b/plugins/modules/devices_info.py index 5e56406..4fd6428 100644 --- a/plugins/modules/devices_info.py +++ b/plugins/modules/devices_info.py @@ -29,7 +29,7 @@ description: - Dictionary of filter key-value pairs to apply on the device details. type: dict - default: None + default: null backup: description: - This argument triggers the module to back up the filtered device's current running-config. @@ -42,7 +42,7 @@ - Directory to store the backup. It's created if missing. Defaults to a 'backup' folder in the current directory. type: path author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) notes: - The C(filters) option allows for specifying filtering criteria such as device model, status, etc. @@ -113,8 +113,8 @@ class BackupPathModel(BaseModel): class ExtendedModuleResult(ModuleResult): - devices: Optional[List] = Field(default=[]) - backup_paths: Optional[List[BackupPathModel]] = Field(default=[]) + devices: Optional[List] = Field(default_factory=list) + backup_paths: Optional[List[BackupPathModel]] = Field(default_factory=list) def run_module(): diff --git a/plugins/modules/devices_wan_edges.py b/plugins/modules/devices_wan_edges.py index 46ffeab..9273996 100644 --- a/plugins/modules/devices_wan_edges.py +++ b/plugins/modules/devices_wan_edges.py @@ -34,7 +34,6 @@ description: - Password for the smart account. type: str - no_log: True wan_edge_list: description: - Filepath to the WAN Edge list for uploading. @@ -51,7 +50,7 @@ type: bool default: False author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) notes: - If 'state' is 'present', either 'sync_devices_from_smart_account' or 'wan_edge_list' must be defined. @@ -142,7 +141,7 @@ class State(str, Enum): class ExtendedModuleResult(ModuleResult): - bootstrap_configuration: Optional[List] = Field(default=[]) + bootstrap_configuration: Optional[List] = Field(default_factory=list) def generate_bootstrap_configuration(module: AnsibleCatalystwanModule, result: ExtendedModuleResult): diff --git a/plugins/modules/feature_profile_builder.py b/plugins/modules/feature_profile_builder.py index 27472c5..e8941c8 100644 --- a/plugins/modules/feature_profile_builder.py +++ b/plugins/modules/feature_profile_builder.py @@ -1,261 +1,279 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright 2024 Cisco Systems, Inc. and its affiliates -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - -DOCUMENTATION = r""" ---- -module: feature_profile_builder -short_description: Description -version_added: "0.3.1" -description: Module for building feature profile data based on parcel templates. -author: - - Przemyslaw Susko (sprzemys@cisco.com) -""" - -RETURN = r""" -msg: - description: Message detailing the outcome of the operation. - returned: on failure - type: str -system_profiles: - description: Generated system feature profiles. - returned: on success - type: str -transport_profiles: - description: Generated transport feature profiles. - returned: on success - type: str -service_profiles: - description: Generated service feature profiles. - returned: on success - type: str -settable_variables: - description: A list of settable variables for created feature profiles grouped by profile type, i.e.: - settable_variables: - service: - interface_names: - - vpn_10_if_0 - static_ip_addresses: - - vpn_10_if_0_static_ipaddr - static_subnets: - - vpn_10_if_0_static_subnet - transport: - interface_names: - - vpn_0_transport_if - returned: on success - type: str -""" - -EXAMPLES = r""" -- name: "Generate config group data from template" - cisco.catalystwan.feature_profile_builder: - templates_path: "/path/to/parcel/templates" - system_profiles: - - name: System - description: Description - parcels: - - template: banner - - template: basic - transport_profiles: - - name: Transport - description: Description - parcels: - wan_vpn_parcel: - template: vpn - config: - name: OverridenName - sub_parcels: - - wan_interface_ethernet_parcel_1: - template: ethernet - config: - data: - interfaceName: - optionType: default - service_profiles: - - name: Service - description: Description - parcels: - - template: vpn - sub_parcels: - - template: ethernet -""" - -import os -from copy import copy -from dataclasses import asdict, dataclass -from typing import Optional - -import yaml -from ansible.module_utils.basic import AnsibleModule - - -class TemplateCache: - def __init__(self, module: AnsibleModule, templates_dir: os.path): - self.module = module - self.dir = templates_dir - self.cache = {} - - def get(self, template): - if template not in self.cache: - file_path = os.path.join(self.dir, template) - try: - with open(file_path, "r") as file: - self.cache[template] = yaml.safe_load(file) - except FileNotFoundError: - self.module.fail_json(msg=f"File not found {file_path}") - except IOError as e: - self.module.fail_json(msg=f"Error reading file {file_path}: {e}") - return copy(self.cache[template]) - - -@dataclass -class SettableVars: - @dataclass - class Vars: - interface_names: set[str] - static_ip_addresses: Optional[set[str]] - static_subnets: Optional[set[str]] - - transport: Vars - service: Vars - - @classmethod - def init(cls): - return cls(transport=SettableVars.Vars(set(), None, None), service=SettableVars.Vars(set(), set(), set())) - - def sort(self): - def _sort_vars(_vars: SettableVars.Vars): - _vars.interface_names = sorted(_vars.interface_names) - if _vars.static_ip_addresses: - _vars.static_ip_addresses = sorted(_vars.static_ip_addresses) - if _vars.static_subnets: - _vars.static_subnets = sorted(_vars.static_subnets) - - _sort_vars(self.transport) - _sort_vars(self.service) - return self - - -SETTABLE_VARS = SettableVars.init() - - -def get_settable_vars_for_profile(profile_type: str) -> SettableVars.Vars: - if profile_type == "transport": - return SETTABLE_VARS.transport - if profile_type == "service": - return SETTABLE_VARS.service - - -def update_template(template, config: dict): - for key, value in config.items(): - if key in template and isinstance(value, dict) and isinstance(template[key], dict): - update_template(template[key], value) - elif key in template or key == "value": - if isinstance(value, str) and value.startswith("{"): - value = "{{ '" + value + "' }}" - template[key] = value - - -def append_settable_vars(config: dict, profile_type: str): - def _helper(_key, _value, _match): - try: - if _key == _match and isinstance(_value, dict) and _value["optionType"] == "variable": - return _value["value"].strip("{ ' }") - except ValueError: - return None - - _settable_vars = get_settable_vars_for_profile(profile_type) - for key, value in config.items(): - if_name = _helper(key, value, "interfaceName") - if if_name: - _settable_vars.interface_names.add(if_name) - continue - - ip_addr = _helper(key, value, "ipAddress") - if ip_addr and (_settable_vars.static_ip_addresses is not None): - _settable_vars.static_ip_addresses.add(ip_addr) - continue - - subnet = _helper(key, value, "subnetMask") - if subnet and (_settable_vars.static_subnets is not None): - _settable_vars.static_subnets.add(subnet) - continue - - if isinstance(value, dict): - append_settable_vars(value, profile_type) - - -def generate_parcel(module: AnsibleModule, cache: TemplateCache, profile_type: str, source: dict): - if "template" not in source: - module.fail_json(f"Template type not provided for {profile_type} type parcel") - - template = cache.get(f"{profile_type}_parcels/{source['template']}.yml") - if "config" in source: - update_template(template["config"], source["config"]) - append_settable_vars(template["config"], profile_type) - - sub_parcels = [] - if source["template"] == "vpn" and "sub_parcels" in source and len(source["sub_parcels"]): - for parcel in source["sub_parcels"]: - sub_parcels.append(generate_parcel(module, cache, profile_type, parcel)) - - generated = {"type": source["template"], "config": template["config"]} - if len(sub_parcels): - generated.update({"sub_parcels": sub_parcels}) - - return generated - - -def generate_profiles(module: AnsibleModule, cache: TemplateCache, profile_type: str, source_profiles: dict): - generated_profiles = [] - for profile in source_profiles: - parcels = [] - for parcel in profile["parcels"]: - parcels.append(generate_parcel(module, cache, profile_type, parcel)) - generated_profiles.append({"name": profile["name"], "description": profile["description"], "parcels": parcels}) - return generated_profiles - - -def run_module(): - module_args = dict( - templates_path=dict(type="str", required=True), - system_profiles=dict(type="list"), - transport_profiles=dict(type="list"), - service_profiles=dict(type="list"), - ) - - result = dict(changed=True, data={}) - - module = AnsibleModule(argument_spec=module_args) - - templates_path = module.params["templates_path"] - system_profiles = module.params["system_profiles"] - transport_profiles = module.params["transport_profiles"] - service_profiles = module.params["service_profiles"] - - cache = TemplateCache(module, templates_path) - generated_system_profiles = generate_profiles(module, cache, "system", system_profiles) - generated_transport_profiles = generate_profiles(module, cache, "transport", transport_profiles) - generated_service_profiles = generate_profiles(module, cache, "service", service_profiles) - - result["data"].update({"system_profiles": generated_system_profiles}) - result["data"].update({"transport_profiles": generated_transport_profiles}) - result["data"].update({"service_profiles": generated_service_profiles}) - result["data"].update( - { - "settable_variables": asdict( - SETTABLE_VARS.sort(), dict_factory=lambda x: {k: v for (k, v) in x if v is not None} - ) - } - ) - module.exit_json(**result) - - -def main(): - run_module() - - -if __name__ == "__main__": - main() +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: feature_profile_builder +short_description: Description +version_added: "0.3.1" +description: Module for building feature profile data based on parcel templates. +options: + templates_path: + description: Path containing the parcel template files. + type: str + required: true + system_profiles: + description: System feature-profile definitions to render. + type: list + elements: dict + transport_profiles: + description: Transport feature-profile definitions to render. + type: list + elements: dict + service_profiles: + description: Service feature-profile definitions to render. + type: list + elements: dict +author: + - Przemyslaw Susko (@sprzemys) +""" + +RETURN = r""" +msg: + description: Message detailing the outcome of the operation. + returned: on failure + type: str +system_profiles: + description: Generated system feature profiles. + returned: on success + type: str +transport_profiles: + description: Generated transport feature profiles. + returned: on success + type: str +service_profiles: + description: Generated service feature profiles. + returned: on success + type: str +settable_variables: + description: >- + A list of settable variables for created feature profiles grouped by profile type. + settable_variables: + service: + interface_names: + - vpn_10_if_0 + static_ip_addresses: + - vpn_10_if_0_static_ipaddr + static_subnets: + - vpn_10_if_0_static_subnet + transport: + interface_names: + - vpn_0_transport_if + returned: on success + type: str +""" + +EXAMPLES = r""" +- name: "Generate config group data from template" + cisco.catalystwan.feature_profile_builder: + templates_path: "/path/to/parcel/templates" + system_profiles: + - name: System + description: Description + parcels: + - template: banner + - template: basic + transport_profiles: + - name: Transport + description: Description + parcels: + wan_vpn_parcel: + template: vpn + config: + name: OverridenName + sub_parcels: + - wan_interface_ethernet_parcel_1: + template: ethernet + config: + data: + interfaceName: + optionType: default + service_profiles: + - name: Service + description: Description + parcels: + - template: vpn + sub_parcels: + - template: ethernet +""" + +import os +from copy import copy +from dataclasses import asdict, dataclass +from typing import Optional + +import yaml +from ansible.module_utils.basic import AnsibleModule + + +class TemplateCache: + def __init__(self, module: AnsibleModule, templates_dir: os.path): + self.module = module + self.dir = templates_dir + self.cache = {} + + def get(self, template): + if template not in self.cache: + file_path = os.path.join(self.dir, template) + try: + with open(file_path, "r") as file: + self.cache[template] = yaml.safe_load(file) + except FileNotFoundError: + self.module.fail_json(msg=f"File not found {file_path}") + except IOError as e: + self.module.fail_json(msg=f"Error reading file {file_path}: {e}") + return copy(self.cache[template]) + + +@dataclass +class SettableVars: + @dataclass + class Vars: + interface_names: set[str] + static_ip_addresses: Optional[set[str]] + static_subnets: Optional[set[str]] + + transport: Vars + service: Vars + + @classmethod + def init(cls): + return cls(transport=SettableVars.Vars(set(), None, None), service=SettableVars.Vars(set(), set(), set())) + + def sort(self): + def _sort_vars(_vars: SettableVars.Vars): + _vars.interface_names = sorted(_vars.interface_names) + if _vars.static_ip_addresses: + _vars.static_ip_addresses = sorted(_vars.static_ip_addresses) + if _vars.static_subnets: + _vars.static_subnets = sorted(_vars.static_subnets) + + _sort_vars(self.transport) + _sort_vars(self.service) + return self + + +SETTABLE_VARS = SettableVars.init() + + +def get_settable_vars_for_profile(profile_type: str) -> SettableVars.Vars: + if profile_type == "transport": + return SETTABLE_VARS.transport + if profile_type == "service": + return SETTABLE_VARS.service + + +def update_template(template, config: dict): + for key, value in config.items(): + if key in template and isinstance(value, dict) and isinstance(template[key], dict): + update_template(template[key], value) + elif key in template or key == "value": + if isinstance(value, str) and value.startswith("{"): + value = "{{ '" + value + "' }}" + template[key] = value + + +def append_settable_vars(config: dict, profile_type: str): + def _helper(_key, _value, _match): + try: + if _key == _match and isinstance(_value, dict) and _value["optionType"] == "variable": + return _value["value"].strip("{ ' }") + except ValueError: + return None + + _settable_vars = get_settable_vars_for_profile(profile_type) + for key, value in config.items(): + if_name = _helper(key, value, "interfaceName") + if if_name: + _settable_vars.interface_names.add(if_name) + continue + + ip_addr = _helper(key, value, "ipAddress") + if ip_addr and (_settable_vars.static_ip_addresses is not None): + _settable_vars.static_ip_addresses.add(ip_addr) + continue + + subnet = _helper(key, value, "subnetMask") + if subnet and (_settable_vars.static_subnets is not None): + _settable_vars.static_subnets.add(subnet) + continue + + if isinstance(value, dict): + append_settable_vars(value, profile_type) + + +def generate_parcel(module: AnsibleModule, cache: TemplateCache, profile_type: str, source: dict): + if "template" not in source: + module.fail_json(f"Template type not provided for {profile_type} type parcel") + + template = cache.get(f"{profile_type}_parcels/{source['template']}.yml") + if "config" in source: + update_template(template["config"], source["config"]) + append_settable_vars(template["config"], profile_type) + + sub_parcels = [] + if source["template"] == "vpn" and "sub_parcels" in source and len(source["sub_parcels"]): + for parcel in source["sub_parcels"]: + sub_parcels.append(generate_parcel(module, cache, profile_type, parcel)) + + generated = {"type": source["template"], "config": template["config"]} + if len(sub_parcels): + generated.update({"sub_parcels": sub_parcels}) + + return generated + + +def generate_profiles(module: AnsibleModule, cache: TemplateCache, profile_type: str, source_profiles: dict): + generated_profiles = [] + for profile in source_profiles: + parcels = [] + for parcel in profile["parcels"]: + parcels.append(generate_parcel(module, cache, profile_type, parcel)) + generated_profiles.append({"name": profile["name"], "description": profile["description"], "parcels": parcels}) + return generated_profiles + + +def run_module(): + module_args = dict( + templates_path=dict(type="str", required=True), + system_profiles=dict(type="list", elements="dict"), + transport_profiles=dict(type="list", elements="dict"), + service_profiles=dict(type="list", elements="dict"), + ) + + result = dict(changed=True, data={}) + + module = AnsibleModule(argument_spec=module_args) + + templates_path = module.params["templates_path"] + system_profiles = module.params["system_profiles"] + transport_profiles = module.params["transport_profiles"] + service_profiles = module.params["service_profiles"] + + cache = TemplateCache(module, templates_path) + generated_system_profiles = generate_profiles(module, cache, "system", system_profiles) + generated_transport_profiles = generate_profiles(module, cache, "transport", transport_profiles) + generated_service_profiles = generate_profiles(module, cache, "service", service_profiles) + + result["data"].update({"system_profiles": generated_system_profiles}) + result["data"].update({"transport_profiles": generated_transport_profiles}) + result["data"].update({"service_profiles": generated_service_profiles}) + result["data"].update( + { + "settable_variables": asdict( + SETTABLE_VARS.sort(), dict_factory=lambda x: {k: v for (k, v) in x if v is not None} + ) + } + ) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/feature_templates.py b/plugins/modules/feature_templates.py index 56f26b8..8648d2b 100644 --- a/plugins/modules/feature_templates.py +++ b/plugins/modules/feature_templates.py @@ -6,7 +6,7 @@ DOCUMENTATION = r""" --- -module: vmanage_feature_template +module: feature_templates short_description: Manage feature templates for Cisco vManage SD-WAN version_added: "0.2.0" description: @@ -63,21 +63,39 @@ - cisco.catalystwan.device_models_feature_template - cisco.catalystwan.manager_authentication author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) """ +EXAMPLES = r""" +- name: Create a Cisco system feature template + cisco.catalystwan.feature_templates: + state: present + template_name: branch-system + template_description: Branch system template + device_models: + - C8000V + cisco_system: {} +""" from typing import Dict, Final, Literal, Optional, get_args from catalystwan.api.template_api import FeatureTemplate from catalystwan.api.templates.device_variable import DeviceVariable from catalystwan.api.templates.models.supported import available_models -from catalystwan.models.common import DeviceModel -from catalystwan.models.templates import FeatureTemplateInformation from catalystwan.session import ManagerHTTPError from catalystwan.typed_list import DataSequence from pydantic import BaseModel, ConfigDict, Field +try: + from catalystwan.dataclasses import FeatureTemplateInfo +except ImportError: + from catalystwan.models.templates import FeatureTemplateInformation as FeatureTemplateInfo + +try: + from catalystwan.utils.device_model import DeviceModel +except ImportError: + from catalystwan.models.common import DeviceModel + from ..module_utils.feature_templates.aaa import aaa_definition from ..module_utils.feature_templates.cisco_aaa import cisco_aaa_definition from ..module_utils.feature_templates.cisco_banner import cisco_banner_definition @@ -107,7 +125,7 @@ class Values(BaseModel): class ExtendedModuleResult(ModuleResult): - templates_info: Optional[Dict] = Field(default={}) + templates_info: Optional[Dict] = Field(default_factory=dict) State = Literal["present", "modified", "absent"] @@ -181,10 +199,10 @@ def run_module(): device_specific_variables: Dict = module.params.get("device_specific_variables") module.logger.info(f"Module input: \n{module.params}\n") - all_templates: DataSequence[FeatureTemplateInformation] = module.get_response_safely( + all_templates: DataSequence[FeatureTemplateInfo] = module.get_response_safely( module.session.api.templates.get, template=FeatureTemplate ) - target_template: Optional[FeatureTemplateInformation] = all_templates.filter(name=template_name) + target_template: Optional[FeatureTemplateInfo] = all_templates.filter(name=template_name) if module.params.get("state") == "present": # Code for checking if template name exists already diff --git a/plugins/modules/feature_templates_info.py b/plugins/modules/feature_templates_info.py index f56fd96..fe59669 100644 --- a/plugins/modules/feature_templates_info.py +++ b/plugins/modules/feature_templates_info.py @@ -93,7 +93,7 @@ default: null type: str author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication notes: @@ -145,16 +145,20 @@ from typing import Dict, Optional from catalystwan.api.template_api import FeatureTemplate -from catalystwan.models.templates import FeatureTemplateInformation from catalystwan.typed_list import DataSequence from pydantic import Field +try: + from catalystwan.dataclasses import FeatureTemplateInfo +except ImportError: + from catalystwan.models.templates import FeatureTemplateInformation as FeatureTemplateInfo + from ..module_utils.result import ModuleResult from ..module_utils.vmanage_module import AnsibleCatalystwanModule class ExtendedModuleResult(ModuleResult): - templates_info: Optional[Dict] = Field(default={}) + templates_info: Optional[Dict] = Field(default_factory=dict) def run_module(): @@ -167,7 +171,7 @@ def run_module(): filters = module.params.get("filters") - all_templates: DataSequence[FeatureTemplateInformation] = module.get_response_safely( + all_templates: DataSequence[FeatureTemplateInfo] = module.get_response_safely( module.session.api.templates.get, template=FeatureTemplate ) @@ -176,13 +180,13 @@ def run_module(): if filtered_templates: module.logger.info(f"All Feature Templates filtered with filters: {filters}:\n{filtered_templates}") result.msg = "Succesfully got all requested Feature Templates Info from vManage" - result.templates_info = [template for template in filtered_templates] + result.templates_info = list(filtered_templates) else: module.logger.warning(msg=f"Feature templates filtered with `{filters}` not present.") result.msg = f"Feature templates filtered with `{filters}` not present on vManage." else: result.msg = "Succesfully got all Feature Templates Info from vManage" - result.templates_info = [template for template in all_templates] + result.templates_info = list(all_templates) module.exit_json(**result.model_dump(mode="json")) diff --git a/plugins/modules/health_checks.py b/plugins/modules/health_checks.py index 46c2aae..e494615 100644 --- a/plugins/modules/health_checks.py +++ b/plugins/modules/health_checks.py @@ -24,7 +24,7 @@ - A dictionary of filters used to select devices for module action. type: dict author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication """ @@ -93,7 +93,7 @@ class ExtendedModuleResult(ModuleResult): - health_summary: Optional[List] = Field(default=[]) + health_summary: Optional[List] = Field(default_factory=list) class HealthCheckTypes(str, Enum): @@ -416,7 +416,7 @@ def run_module(): module_args = dict( check_type=dict( type=str, - choices=[check_type for check_type in HealthCheckTypes], + choices=list(HealthCheckTypes), required=True, ), filters=dict(type="dict", default=None), diff --git a/plugins/modules/policy.py b/plugins/modules/policy.py index f561adf..26abd7f 100644 --- a/plugins/modules/policy.py +++ b/plugins/modules/policy.py @@ -36,24 +36,24 @@ description: - Centralized policy definition - The centralized, localized, definition and list options are mutually exclusive. - type: dictionary + type: dict localized: description: - Localized policy definition - The centralized, localized, definition and list options are mutually exclusive. - type: dictionary + type: dict definition: description: - Policy definition object - The centralized, localized, definition and list options are mutually exclusive. - type: dictionary + type: dict list: description: - Policy list object - The centralized, localized, definition and list options are mutually exclusive. - type: dictionary + type: dict author: - - Piotr Piwowarski (pipiwowa@cisco.com) + - Piotr Piwowarski (@pipiwowa) extends_documentation_fragment: - cisco.catalystwan.manager_authentication @@ -111,11 +111,10 @@ AnyPolicyDefinition, AnyPolicyList, CentralizedPolicy, - CentralizedPolicyInfo, LocalizedPolicy, - LocalizedPolicyInfo, ) -from catalystwan.models.policy.centralized import CentralizedPolicyEditPayload +from catalystwan.models.policy.centralized import CentralizedPolicyEditPayload, CentralizedPolicyInfo +from catalystwan.models.policy.localized import LocalizedPolicyInfo from catalystwan.session import ManagerHTTPError from catalystwan.typed_list import DataSequence @@ -183,6 +182,8 @@ def run_module(): object_name: str = module.params.get("name") object_description: str = module.params.get("description") + existing_object_id = None + object_to_create = None if module.params.get("centralized"): object_pretty_name = "Centralized Policy" diff --git a/plugins/modules/server_info.py b/plugins/modules/server_info.py index 63059e2..479c5c9 100644 --- a/plugins/modules/server_info.py +++ b/plugins/modules/server_info.py @@ -21,7 +21,7 @@ choices: ["server_info", "server_ready", "about_info"] default: "server_info" author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) notes: - The module does not make any changes on the server, it only retrieves information. @@ -40,7 +40,7 @@ description: The detailed server information based on the selected category. returned: success type: dict - sample: {"version": "20.3.1", "buildNumber": "12345", "ready": true} + sample: {"platform_version": "26.1.0", "is_server_ready": true} msg: description: Failure message if the information could not be retrieved. returned: failure @@ -98,7 +98,10 @@ def run_module(): elif module.params["information_category"] == InformationCategory.ABOUT_INFO: response = module.get_response_safely(module.session.endpoints.client.about) - result.response = response.dict() + try: + result.response = response.model_dump(mode="json") + except AttributeError: # catalystwan models created with Pydantic v1 + result.response = response.dict() module.exit_json(**result.model_dump(mode="json")) diff --git a/plugins/modules/software_repository.py b/plugins/modules/software_repository.py index 13e86cf..b0817cd 100644 --- a/plugins/modules/software_repository.py +++ b/plugins/modules/software_repository.py @@ -57,9 +57,8 @@ aliases: [ 'port' ] remote_server_vpn: description: - - VPN ID used by the remote server. + - VPN ID used by the remote server, from 0 through 65527. type: int - choices: range(0, 65528) aliases: [ 'vpn' ] remote_server_user: description: @@ -70,7 +69,6 @@ description: - Password to authenticate to the remote server. type: str - no_log: true aliases: [ 'password' ] image_location_prefix: description: @@ -111,7 +109,7 @@ aliases: [ 'filename' ] author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication """ @@ -255,7 +253,7 @@ def run_module(): remote_server_url=dict(type="str", aliases=["url"]), remote_server_protocol=dict( type="str", - choices=[choice for choice in RemoteServerProtocol], + choices=list(RemoteServerProtocol), default=RemoteServerProtocol.FTP.value, aliases=["protocol"], ), @@ -279,7 +277,7 @@ def run_module(): ), ("state", State.ABSENT.value, ("remote_server_id",), False), ], - mutually_exclusive=[("remote_server_name", "id")], + mutually_exclusive=[("remote_server_name", "remote_server_id")], ), software=dict( type="dict", @@ -313,6 +311,8 @@ def run_module(): upload_software_from_remote_server = False delete_software_from_software_repository = False software_payload = None + existing_remote_server_id = None + remove_software_id = None # ---------------------------------# # STEP 1 - verify module arguments # @@ -356,20 +356,21 @@ def run_module(): update_remote_server = True if module.params["remote_server"]["state"] == State.ABSENT.value: - remote_server_id = module.params["remote_server"]["id"] - existing_server: RemoteServerInfo = remote_servers.filter(remote_server_id=remote_server_id) + remote_server_id = module.params["remote_server"]["remote_server_id"] + existing_server: RemoteServerInfo = remote_servers.filter( + remote_server_id=remote_server_id + ).single_or_default() if existing_server: remove_remote_server = True else: - result.response[ - "remove_remote_server" - ] = f"Server with UUID: {remote_server_id} not present in Remote Servers List" + result.response["remove_remote_server"] = ( + f"Server with UUID: {remote_server_id} not present in Remote Servers List" + ) if module.params.get("software"): image_path = module.params["software"].get("image_path") - remote_server_id = module.params["software"].get("remote_server_id") remote_server_name = module.params["software"].get("remote_server_name") - remote_filename = module.params["software"].get("filename") + remote_filename = module.params["software"].get("remote_filename") software_id = module.params["software"].get("software_id") software_state = module.params["software"]["state"] @@ -393,11 +394,7 @@ def run_module(): f"{version_in_available_files}, skipping upload." ) - elif ( - software_state == State.PRESENT.value - and remote_filename # noqa: W503 - and (remote_server_id or remote_server_name) # noqa: W503 - ): + elif software_state == State.PRESENT.value and remote_filename and remote_server_name: remote_servers = module.get_response_safely( module.session.endpoints.configuration_software_actions.get_list_of_remote_servers ) diff --git a/plugins/modules/software_repository_info.py b/plugins/modules/software_repository_info.py index 9f0ba16..02b914c 100644 --- a/plugins/modules/software_repository_info.py +++ b/plugins/modules/software_repository_info.py @@ -23,9 +23,9 @@ description: - Optional filters used to refine the results. type: dict - default: None + default: null author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication """ @@ -68,8 +68,8 @@ class ExtendedModuleResult(ModuleResult): - remote_servers: Optional[List] = Field(default=[]) - software_images: Optional[List] = Field(default=[]) + remote_servers: Optional[List] = Field(default_factory=list) + software_images: Optional[List] = Field(default_factory=list) def run_module(): diff --git a/plugins/modules/software_upgrade.py b/plugins/modules/software_upgrade.py index 175b7c5..fe1fd00 100644 --- a/plugins/modules/software_upgrade.py +++ b/plugins/modules/software_upgrade.py @@ -76,7 +76,7 @@ type: list elements: str author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) notes: - This module does not guarantee idempotency - certain operations like installation will always change device state. @@ -205,6 +205,10 @@ class SoftwareState(str, Enum): DEFAULT = "default" # in vManage -> DEFAULT +def _all_devices_are_managers(devices: DataSequence[DeviceDetailsResponse]) -> bool: + return all(device.personality == "vmanage" for device in devices) + + @retry( wait=wait_fixed(INTERVAL_SECONDS), stop=stop_after_attempt(int(TIMEOUT_SECONDS / INTERVAL_SECONDS)), @@ -316,7 +320,7 @@ def run_module(): if ( module.params.get("wait_for_completed") and module.params.get("reboot") # noqa: W503 - and all([True for device in devices if device.personality == "vmanage"]) # noqa: W503 + and _all_devices_are_managers(devices) # noqa: W503 ): try: module.session.restart_imminent(restart_timeout_override=module.params.get("wait_timeout_seconds")) @@ -381,9 +385,7 @@ def run_module(): version_to_activate=module.params.get("image_version"), ) - if module.params.get("wait_for_completed") and all( - [True for device in devices if device.personality == "vmanage"] - ): + if module.params.get("wait_for_completed") and _all_devices_are_managers(devices): try: module.session.restart_imminent(restart_timeout_override=module.params.get("wait_timeout_seconds")) wait_for_task_data(module=module, result=result, task=activate_task) diff --git a/plugins/modules/software_upgrade_info.py b/plugins/modules/software_upgrade_info.py index 0e0001d..11cf1f9 100644 --- a/plugins/modules/software_upgrade_info.py +++ b/plugins/modules/software_upgrade_info.py @@ -28,9 +28,9 @@ description: - Optional filters to apply on the list of installed devices. type: dict - default: None + default: null author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication """ @@ -96,7 +96,7 @@ class ExtendedModuleResult(ModuleResult): - installed_devices: Optional[List] = Field(default=[]) + installed_devices: Optional[List] = Field(default_factory=list) def run_module(): diff --git a/plugins/modules/tenants.py b/plugins/modules/tenants.py index 1fb3d7f..5817969 100644 --- a/plugins/modules/tenants.py +++ b/plugins/modules/tenants.py @@ -30,7 +30,7 @@ - The description of the Tenant. required: false type: str - default: None + default: null org_name: description: - The organization name of the Tenant. @@ -51,7 +51,7 @@ - The timeout in seconds for creating Tenant. Default is 7200. type: int author: - - Piotr Piwowarski (pipiwowa@cisco.com) + - Piotr Piwowarski (@pipiwowa) extends_documentation_fragment: - cisco.catalystwan.manager_authentication notes: diff --git a/plugins/modules/tenants_info.py b/plugins/modules/tenants_info.py index 5c2e880..1b93852 100644 --- a/plugins/modules/tenants_info.py +++ b/plugins/modules/tenants_info.py @@ -12,6 +12,8 @@ version_added: "0.3.5" description: - This module allows you to get tenants Info from vManage. +author: + - Piotr Piwowarski (@pipiwowa) options: filters: description: @@ -113,9 +115,9 @@ def run_module(): result.tenancy_domain = tenancy_mode.domain if module.params.get("filters"): - result.tenants_info = [tenant for tenant in all_tenants.filter(**filters)] + result.tenants_info = list(all_tenants.filter(**filters)) else: - result.tenants_info = [tenant for tenant in all_tenants] + result.tenants_info = list(all_tenants) if result.tenants_info: module.logger.info(f"All tenants filtered with filters: {filters}:\n{result.tenants_info}") diff --git a/plugins/modules/users.py b/plugins/modules/users.py index 93c612c..1c95dba 100644 --- a/plugins/modules/users.py +++ b/plugins/modules/users.py @@ -52,7 +52,7 @@ - Ensure that the provided credentials have sufficient permissions to manage users in vManage. - Passwords should be handled carefully, consider using Ansible Vault for sensitive data. author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication """ @@ -121,7 +121,7 @@ class ExtendedModuleResult(ModuleResult): - users: Optional[List] = Field(default=[]) + users: Optional[List] = Field(default_factory=list) def run_module(): diff --git a/plugins/modules/vmanage_mode.py b/plugins/modules/vmanage_mode.py index defbe8f..3f29b61 100644 --- a/plugins/modules/vmanage_mode.py +++ b/plugins/modules/vmanage_mode.py @@ -28,7 +28,7 @@ elements: str required: true author: - - Arkadiusz Cichon (acichon@cisco.com) + - Arkadiusz Cichon (@acichon) extends_documentation_fragment: - cisco.catalystwan.manager_authentication notes: @@ -83,7 +83,7 @@ class ExtendedModuleResult(ModuleResult): - attached_templates: Optional[Dict] = Field(default={}) + attached_templates: Optional[Dict] = Field(default_factory=dict) def run_module(): diff --git a/plugins/modules/wait_for_api_server.py b/plugins/modules/wait_for_api_server.py index debb396..eb7be1f 100644 --- a/plugins/modules/wait_for_api_server.py +++ b/plugins/modules/wait_for_api_server.py @@ -6,7 +6,7 @@ DOCUMENTATION = r""" --- -module: XXX +module: wait_for_api_server short_description: XXX diff --git a/pyproject.toml b/pyproject.toml index efa396d..c0126f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,17 +6,16 @@ authors = ["acichon "] readme = "README.md" [tool.poetry.dependencies] -python = "^3.10" -ansible-core = "2.16.6" -ansible = "9.4.0" -catalystwan = "v0.35.5dev3" -flake8 = "5.0.4" -black = "24.3.0" -pre-commit = "3.7" -pydantic = "2.7" -requests = "2.32.2" -urllib3 = "2.2.2" -ansible-lint = { version = "6.22.2", markers = "platform_system != 'Windows'" } +python = ">=3.12,<3.15" +ansible = "14.3.1" +catalystwan = "0.41.5.dev2" +flake8 = "7.3.0" +black = "26.5.1" +pre-commit = "4.6.2" +pydantic = ">=2.7,<3.0" +requests = ">=2.32.5,<3.0" +urllib3 = ">=2.6,<3.0" +ansible-lint = { version = "26.8.0", markers = "platform_system != 'Windows'" } [tool.poetry.group.dev.dependencies] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..7f74178 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +-r requirements.txt +ansible-lint==26.8.0 +black==26.5.1 +flake8==7.3.0 +pre-commit==4.6.2 +pycln==2.6.0 +pytest==9.1.1 diff --git a/requirements.txt b/requirements.txt index 0f95768..9788006 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,70 +1,7 @@ -annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0" -ansible-compat==24.7.0 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -ansible-core==2.16.6 ; python_version >= "3.10" and python_version < "4.0" -ansible-lint==6.22.2 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -ansible==9.4.0 ; python_version >= "3.10" and python_version < "4.0" -args==0.1.0 ; python_version >= "3.10" and python_version < "4.0" -attrs==21.4.0 ; python_version >= "3.10" and python_version < "4.0" -black==24.3.0 ; python_version >= "3.10" and python_version < "4.0" -bracex==2.5 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -catalystwan==v0.40.1dev1 ; python_version >= "3.10" and python_version < "4.0" -certifi==2024.7.4 ; python_version >= "3.10" and python_version < "4.0" -cffi==1.17.0 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy" -cfgv==3.4.0 ; python_version >= "3.10" and python_version < "4.0" -charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0" -ciscoconfparse==1.9.41 ; python_version >= "3.10" and python_version < "4.0" -click==8.1.7 ; python_version >= "3.10" and python_version < "4.0" -clint==0.5.1 ; python_version >= "3.10" and python_version < "4.0" -colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and (platform_system == "Windows" or sys_platform == "win32") -cryptography==44.0.1 ; python_version >= "3.10" and python_version < "4.0" -deprecated==1.2.14 ; python_version >= "3.10" and python_version < "4.0" -distlib==0.3.8 ; python_version >= "3.10" and python_version < "4.0" -dnspython==2.6.1 ; python_version >= "3.10" and python_version < "4.0" -filelock==3.15.4 ; python_version >= "3.10" and python_version < "4.0" -flake8-quotes==3.4.0 ; python_version >= "3.10" and python_version < "4.0" -flake8==5.0.4 ; python_version >= "3.10" and python_version < "4.0" -hier-config==2.2.3 ; python_version >= "3.10" and python_version < "4.0" -identify==2.6.0 ; python_version >= "3.10" and python_version < "4.0" -idna==3.7 ; python_version >= "3.10" and python_version < "4.0" -jinja2==3.1.6 ; python_version >= "3.10" and python_version < "4.0" -jsonschema==4.17.3 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -loguru==0.7.2 ; python_version >= "3.10" and python_version < "4.0" -markdown-it-py==3.0.0 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "4.0" -mccabe==0.7.0 ; python_version >= "3.10" and python_version < "4.0" -mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -mypy-extensions==1.0.0 ; python_version >= "3.10" and python_version < "4.0" -nodeenv==1.9.1 ; python_version >= "3.10" and python_version < "4.0" -packaging==23.2 ; python_version >= "3.10" and python_version < "4.0" -passlib==1.7.4 ; python_version >= "3.10" and python_version < "4.0" -pathspec==0.12.1 ; python_version >= "3.10" and python_version < "4.0" -platformdirs==4.2.2 ; python_version >= "3.10" and python_version < "4.0" -pre-commit==3.7.0 ; python_version >= "3.10" and python_version < "4.0" -pycodestyle==2.9.1 ; python_version >= "3.10" and python_version < "4.0" -pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0" and platform_python_implementation != "PyPy" -pydantic-core==2.18.1 ; python_version >= "3.10" and python_version < "4.0" -pydantic==2.7.0 ; python_version >= "3.10" and python_version < "4.0" -pyflakes==2.5.0 ; python_version >= "3.10" and python_version < "4.0" -pygments==2.18.0 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -pyrsistent==0.20.0 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0" -pyyaml==6.0.2 ; python_version >= "3.10" and python_version < "4.0" -requests-toolbelt==1.0.0 ; python_version >= "3.10" and python_version < "4.0" -requests==2.32.2 ; python_version >= "3.10" and python_version < "4.0" -resolvelib==1.0.1 ; python_version >= "3.10" and python_version < "4.0" -rich==13.7.1 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -ruamel-yaml-clib==0.2.8 ; platform_python_implementation == "CPython" and python_version < "3.13" and python_version >= "3.10" and platform_system != "Windows" -ruamel-yaml==0.18.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -setuptools==78.1.1 ; python_version >= "3.10" and python_version < "4.0" -six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" -subprocess-tee==0.4.2 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -tenacity==8.3.0 ; python_version >= "3.10" and python_version < "4.0" -toml==0.10.2 ; python_version >= "3.10" and python_version < "4.0" -tomli==2.0.1 ; python_version >= "3.10" and python_version < "3.11" -typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4.0" -urllib3==2.2.2 ; python_version >= "3.10" and python_version < "4.0" -virtualenv==20.26.6 ; python_version >= "3.10" and python_version < "4.0" -wcmatch==9.0 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" -win32-setctime==1.1.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" -wrapt==1.16.0 ; python_version >= "3.10" and python_version < "4.0" -yamllint==1.35.1 ; python_version >= "3.10" and python_version < "4.0" and platform_system != "Windows" +# Tested runtime stack. Python 3.12-3.14 is required by Ansible 14. +ansible==14.3.1 + +# The 0.41.5 development line is the newest published SDK artifact that still +# contains every high-level API used by this collection. Compatibility tests +# also cover the latest stable SDK, 0.41.6, for modules whose APIs remain. +catalystwan==0.41.5.dev2 diff --git a/roles/config_groups/tasks/edge_variables.yml b/roles/config_groups/tasks/edge_variables.yml index 1d63dee..8b1e9fa 100644 --- a/roles/config_groups/tasks/edge_variables.yml +++ b/roles/config_groups/tasks/edge_variables.yml @@ -1,46 +1,46 @@ -# Copyright 2024 Cisco Systems, Inc. and its affiliates -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - ---- -- name: Set device_vars fact - ansible.builtin.set_fact: - device_vars: '{{ device | ansible.utils.remove_keys(target=[''admin_username'', ''mgmt_public_ip'', ''transport_public_ip'', ''service_interfaces'']) }}' - -- name: Set transport interface names - ansible.builtin.set_fact: - device_vars: "{{ device_vars | combine({ item: 'GigabitEthernet' ~ (index + 1) | string }) }}" - loop_control: - index_var: index - loop: "{{ settable_variables.transport.interface_names }}" - -- name: Set service interface names - ansible.builtin.set_fact: - device_vars: "{{ device_vars | combine({ item: 'GigabitEthernet' ~ device.service_interfaces[index].index }) }}" - loop_control: - index_var: index - loop: "{{ settable_variables.service.interface_names }}" - -- name: Set service static IP addresses - ansible.builtin.set_fact: - device_vars: "{{ device_vars | combine({key: device.service_interfaces[index].addr}) }}" - vars: - key: "{{ item }}" - loop: "{{ settable_variables.service.static_ip_addresses }}" - loop_control: - index_var: index - -- name: Set service static subnets - ansible.builtin.set_fact: - device_vars: "{{ device_vars | combine({key: value}) }}" - vars: - key: "{{ item }}" - value: "255.255.255.0" - loop: "{{ settable_variables.service.static_subnets }}" - -- name: Set remaining variables - ansible.builtin.set_fact: - device_vars: "{{ device_vars | combine({'pseudo_commit_timer': pseudo_commit_timer}) }}" - -- name: Append to edge_device_variables - ansible.builtin.set_fact: - edge_device_variables: "{{ edge_device_variables + [device_vars] }}" +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- +- name: Set device_vars fact + ansible.builtin.set_fact: + device_vars: '{{ device | ansible.utils.remove_keys(target=[''admin_username'', ''mgmt_public_ip'', ''transport_public_ip'', ''service_interfaces'']) }}' + +- name: Set transport interface names + ansible.builtin.set_fact: + device_vars: "{{ device_vars | combine({ item: 'GigabitEthernet' ~ (index + 1) | string }) }}" + loop_control: + index_var: index + loop: "{{ settable_variables.transport.interface_names }}" + +- name: Set service interface names + ansible.builtin.set_fact: + device_vars: "{{ device_vars | combine({ item: 'GigabitEthernet' ~ device.service_interfaces[index].index }) }}" + loop_control: + index_var: index + loop: "{{ settable_variables.service.interface_names }}" + +- name: Set service static IP addresses + ansible.builtin.set_fact: + device_vars: "{{ device_vars | combine({key: device.service_interfaces[index].addr}) }}" + vars: + key: "{{ item }}" + loop: "{{ settable_variables.service.static_ip_addresses }}" + loop_control: + index_var: index + +- name: Set service static subnets + ansible.builtin.set_fact: + device_vars: "{{ device_vars | combine({key: value}) }}" + vars: + key: "{{ item }}" + value: "255.255.255.0" + loop: "{{ settable_variables.service.static_subnets }}" + +- name: Set remaining variables + ansible.builtin.set_fact: + device_vars: "{{ device_vars | combine({'pseudo_commit_timer': pseudo_commit_timer}) }}" + +- name: Append to edge_device_variables + ansible.builtin.set_fact: + edge_device_variables: "{{ edge_device_variables + [device_vars] }}" diff --git a/roles/feature_profile_builder/templates/service_parcels/ethernet.yml b/roles/feature_profile_builder/templates/service_parcels/ethernet.yml index b4fe8b3..9f4c779 100644 --- a/roles/feature_profile_builder/templates/service_parcels/ethernet.yml +++ b/roles/feature_profile_builder/templates/service_parcels/ethernet.yml @@ -1,41 +1,41 @@ -type: ethernet -config: - name: Ethernet_Parcel - description: Ethernet Parcel - data: - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{if_name}}' }}" - description: - optionType: default - intfIpAddress: - static: - staticIpV4AddressPrimary: - ipAddress: - optionType: variable - value: "{{ '{{static_ipaddr}}' }}" - subnetMask: - optionType: variable - value: "{{ '{{static_subnet}}' }}" - nat: - optionType: default - value: false - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default +type: ethernet +config: + name: Ethernet_Parcel + description: Ethernet Parcel + data: + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{if_name}}' }}" + description: + optionType: default + intfIpAddress: + static: + staticIpV4AddressPrimary: + ipAddress: + optionType: variable + value: "{{ '{{static_ipaddr}}' }}" + subnetMask: + optionType: variable + value: "{{ '{{static_subnet}}' }}" + nat: + optionType: default + value: false + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default value: true \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/service_parcels/vpn.yml b/roles/feature_profile_builder/templates/service_parcels/vpn.yml index c7adfee..d7559ad 100644 --- a/roles/feature_profile_builder/templates/service_parcels/vpn.yml +++ b/roles/feature_profile_builder/templates/service_parcels/vpn.yml @@ -1,15 +1,15 @@ -type: vpn -config: - name: VPN_Parcel - description: VPN Parcel - data: - vpnId: - optionType: default - value: 10 - name: - optionType: default - value: name - ipv4Route: [] - enableSdra: - optionType: global +type: vpn +config: + name: VPN_Parcel + description: VPN Parcel + data: + vpnId: + optionType: default + value: 10 + name: + optionType: default + value: name + ipv4Route: [] + enableSdra: + optionType: global value: false \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/system_parcels/aaa.yml b/roles/feature_profile_builder/templates/system_parcels/aaa.yml index 0814b53..8d91847 100644 --- a/roles/feature_profile_builder/templates/system_parcels/aaa.yml +++ b/roles/feature_profile_builder/templates/system_parcels/aaa.yml @@ -1,31 +1,31 @@ -type: aaa -config: - name: AAA - description: AAA Profile Feature Description - data: - authenticationGroup: - optionType: default - value: false - accountingGroup: - optionType: default - value: false - serverAuthOrder: - optionType: global - value: - - local - authorizationConsole: - optionType: default - value: false - authorizationConfigCommands: - optionType: default - value: false - user: - - name: - optionType: global - value: admin - password: - optionType: variable - value: "{{ '{{admin_password}}' }}" - privilege: - optionType: default - value: '15' +type: aaa +config: + name: AAA + description: AAA Profile Feature Description + data: + authenticationGroup: + optionType: default + value: false + accountingGroup: + optionType: default + value: false + serverAuthOrder: + optionType: global + value: + - local + authorizationConsole: + optionType: default + value: false + authorizationConfigCommands: + optionType: default + value: false + user: + - name: + optionType: global + value: admin + password: + optionType: variable + value: "{{ '{{admin_password}}' }}" + privilege: + optionType: default + value: '15' diff --git a/roles/feature_profile_builder/templates/system_parcels/banner.yml b/roles/feature_profile_builder/templates/system_parcels/banner.yml index f5d6708..2463db5 100644 --- a/roles/feature_profile_builder/templates/system_parcels/banner.yml +++ b/roles/feature_profile_builder/templates/system_parcels/banner.yml @@ -1,11 +1,11 @@ -type: banner -config: - name: Banner - description: Banner Description - data: - login: - optionType: default - value: "" - motd: - optionType: default +type: banner +config: + name: Banner + description: Banner Description + data: + login: + optionType: default + value: "" + motd: + optionType: default value: "" \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/system_parcels/basic.yml b/roles/feature_profile_builder/templates/system_parcels/basic.yml index 4ed05ea..a7c516a 100644 --- a/roles/feature_profile_builder/templates/system_parcels/basic.yml +++ b/roles/feature_profile_builder/templates/system_parcels/basic.yml @@ -1,76 +1,76 @@ -type: basic -config: - name: Basic - description: Basic Setting Description - data: - affinityGroupNumber: - optionType: default - affinityGroupPreference: - optionType: default - affinityPerVrf: - - affinityGroupNumber: - optionType: default - vrfRange: - optionType: default - affinityPreferenceAuto: - optionType: default - value: false - siteType: - optionType: default - clock: - timezone: - optionType: default - value: UTC - description: - optionType: default - location: - optionType: default - gpsLocation: - latitude: - optionType: default - longitude: - optionType: default - deviceGroups: - optionType: default - controllerGroupList: - optionType: default - overlayId: - optionType: default - value: 1 - portOffset: - optionType: default - value: 0 - portHop: - optionType: default - value: true - controlSessionPps: - optionType: default - value: 300 - trackTransport: - optionType: default - value: true - trackInterfaceTag: - optionType: default - consoleBaudRate: - optionType: default - value: '9600' - maxOmpSessions: - optionType: default - multiTenant: - optionType: default - value: false - trackDefaultGateway: - optionType: default - value: true - adminTechOnFailure: - optionType: default - value: true - idleTimeout: - optionType: default - onDemand: - onDemandEnable: - optionType: default - value: false - onDemandIdleTimeout: - optionType: default +type: basic +config: + name: Basic + description: Basic Setting Description + data: + affinityGroupNumber: + optionType: default + affinityGroupPreference: + optionType: default + affinityPerVrf: + - affinityGroupNumber: + optionType: default + vrfRange: + optionType: default + affinityPreferenceAuto: + optionType: default + value: false + siteType: + optionType: default + clock: + timezone: + optionType: default + value: UTC + description: + optionType: default + location: + optionType: default + gpsLocation: + latitude: + optionType: default + longitude: + optionType: default + deviceGroups: + optionType: default + controllerGroupList: + optionType: default + overlayId: + optionType: default + value: 1 + portOffset: + optionType: default + value: 0 + portHop: + optionType: default + value: true + controlSessionPps: + optionType: default + value: 300 + trackTransport: + optionType: default + value: true + trackInterfaceTag: + optionType: default + consoleBaudRate: + optionType: default + value: '9600' + maxOmpSessions: + optionType: default + multiTenant: + optionType: default + value: false + trackDefaultGateway: + optionType: default + value: true + adminTechOnFailure: + optionType: default + value: true + idleTimeout: + optionType: default + onDemand: + onDemandEnable: + optionType: default + value: false + onDemandIdleTimeout: + optionType: default value: 10 \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/system_parcels/bfd.yml b/roles/feature_profile_builder/templates/system_parcels/bfd.yml index ab6ee6a..50b4f28 100644 --- a/roles/feature_profile_builder/templates/system_parcels/bfd.yml +++ b/roles/feature_profile_builder/templates/system_parcels/bfd.yml @@ -1,14 +1,14 @@ -type: bfd -config: - name: BFD - description: BFD Description - data: - pollInterval: - optionType: default - value: 600000 - multiplier: - optionType: default - value: 6 - defaultDscp: - optionType: default +type: bfd +config: + name: BFD + description: BFD Description + data: + pollInterval: + optionType: default + value: 600000 + multiplier: + optionType: default + value: 6 + defaultDscp: + optionType: default value: 48 \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/system_parcels/global.yml b/roles/feature_profile_builder/templates/system_parcels/global.yml index f077dba..87cd737 100644 --- a/roles/feature_profile_builder/templates/system_parcels/global.yml +++ b/roles/feature_profile_builder/templates/system_parcels/global.yml @@ -1,73 +1,73 @@ -type: global -config: - name: Global - description: Global Description - data: - services_global: - services_ip: - servicesGlobalServicesIpHttpServer: - optionType: default - value: false - servicesGlobalServicesIpHttpsServer: - optionType: default - value: false - servicesGlobalServicesIpFtpPassive: - optionType: default - value: false - servicesGlobalServicesIpDomainLookup: - optionType: default - value: false - servicesGlobalServicesIpArpProxy: - optionType: default - value: false - servicesGlobalServicesIpRcmd: - optionType: default - value: false - servicesGlobalServicesIpLineVty: - optionType: default - value: false - servicesGlobalServicesIpCdp: - optionType: default - value: true - servicesGlobalServicesIpLldp: - optionType: default - value: true - servicesGlobalServicesIpSourceIntrf: - optionType: default - globalOtherSettingsTcpKeepalivesIn: - optionType: default - value: true - globalOtherSettingsTcpKeepalivesOut: - optionType: default - value: true - globalOtherSettingsTcpSmallServers: - optionType: default - value: false - globalOtherSettingsUdpSmallServers: - optionType: default - value: false - globalOtherSettingsConsoleLogging: - optionType: default - value: true - globalOtherSettingsIPSourceRoute: - optionType: default - value: false - globalOtherSettingsVtyLineLogging: - optionType: default - value: false - globalOtherSettingsSnmpIfindexPersist: - optionType: default - value: true - globalOtherSettingsIgnoreBootp: - optionType: default - value: true - globalSettingsNat64UdpTimeout: - optionType: default - value: 300 - globalSettingsNat64TcpTimeout: - optionType: default - value: 3600 - globalSettingsHttpAuthentication: - optionType: default - globalSettingsSSHVersion: +type: global +config: + name: Global + description: Global Description + data: + services_global: + services_ip: + servicesGlobalServicesIpHttpServer: + optionType: default + value: false + servicesGlobalServicesIpHttpsServer: + optionType: default + value: false + servicesGlobalServicesIpFtpPassive: + optionType: default + value: false + servicesGlobalServicesIpDomainLookup: + optionType: default + value: false + servicesGlobalServicesIpArpProxy: + optionType: default + value: false + servicesGlobalServicesIpRcmd: + optionType: default + value: false + servicesGlobalServicesIpLineVty: + optionType: default + value: false + servicesGlobalServicesIpCdp: + optionType: default + value: true + servicesGlobalServicesIpLldp: + optionType: default + value: true + servicesGlobalServicesIpSourceIntrf: + optionType: default + globalOtherSettingsTcpKeepalivesIn: + optionType: default + value: true + globalOtherSettingsTcpKeepalivesOut: + optionType: default + value: true + globalOtherSettingsTcpSmallServers: + optionType: default + value: false + globalOtherSettingsUdpSmallServers: + optionType: default + value: false + globalOtherSettingsConsoleLogging: + optionType: default + value: true + globalOtherSettingsIPSourceRoute: + optionType: default + value: false + globalOtherSettingsVtyLineLogging: + optionType: default + value: false + globalOtherSettingsSnmpIfindexPersist: + optionType: default + value: true + globalOtherSettingsIgnoreBootp: + optionType: default + value: true + globalSettingsNat64UdpTimeout: + optionType: default + value: 300 + globalSettingsNat64TcpTimeout: + optionType: default + value: 3600 + globalSettingsHttpAuthentication: + optionType: default + globalSettingsSSHVersion: optionType: default \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/system_parcels/logging.yml b/roles/feature_profile_builder/templates/system_parcels/logging.yml index 7f7d2f1..69885d8 100644 --- a/roles/feature_profile_builder/templates/system_parcels/logging.yml +++ b/roles/feature_profile_builder/templates/system_parcels/logging.yml @@ -1,13 +1,13 @@ -type: logging -config: - name: Logging - description: Logging Description - data: - disk: - file: - diskFileSize: - optionType: default - value: 10 - diskFileRotate: - optionType: default +type: logging +config: + name: Logging + description: Logging Description + data: + disk: + file: + diskFileSize: + optionType: default + value: 10 + diskFileRotate: + optionType: default value: 10 \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/system_parcels/mrf.yml b/roles/feature_profile_builder/templates/system_parcels/mrf.yml index 7d3b127..f7472cc 100644 --- a/roles/feature_profile_builder/templates/system_parcels/mrf.yml +++ b/roles/feature_profile_builder/templates/system_parcels/mrf.yml @@ -1,8 +1,8 @@ -type: mrf -config: - name: MRF - description: Multi Region Fabric Description - data: - role: - optionType: global +type: mrf +config: + name: MRF + description: Multi Region Fabric Description + data: + role: + optionType: global value: edge-router \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/system_parcels/ntp.yml b/roles/feature_profile_builder/templates/system_parcels/ntp.yml index 4118ff0..d0baae3 100644 --- a/roles/feature_profile_builder/templates/system_parcels/ntp.yml +++ b/roles/feature_profile_builder/templates/system_parcels/ntp.yml @@ -1,22 +1,22 @@ -type: ntp -config: - name: NTP - description: NTP Description - data: - server: - - name: - optionType: global - value: time.google.com - vpn: - optionType: default - value: 0 - version: - optionType: default - value: 4 - key: - optionType: default - sourceInterface: - optionType: default - prefer: - optionType: default +type: ntp +config: + name: NTP + description: NTP Description + data: + server: + - name: + optionType: global + value: time.google.com + vpn: + optionType: default + value: 0 + version: + optionType: default + value: 4 + key: + optionType: default + sourceInterface: + optionType: default + prefer: + optionType: default value: false \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/system_parcels/omp.yml b/roles/feature_profile_builder/templates/system_parcels/omp.yml index e3e0ca7..2d5f281 100644 --- a/roles/feature_profile_builder/templates/system_parcels/omp.yml +++ b/roles/feature_profile_builder/templates/system_parcels/omp.yml @@ -1,91 +1,91 @@ -type: omp -config: - name: OMP - description: OMP Description - data: - gracefulRestart: - optionType: default - value: true - sendPathLimit: - optionType: default - value: 4 - ecmpLimit: - optionType: default - value: 4 - advertisementInterval: - optionType: default - value: 1 - holdtime: - optionType: default - value: 60 - eorTimer: - optionType: default - value: 300 - overlayAs: - optionType: default - shutdown: - optionType: default - value: false - gracefulRestartTimer: - optionType: default - value: 43200 - ompAdminDistanceIpv4: - optionType: default - value: 251 - ompAdminDistanceIpv6: - optionType: default - value: 251 - advertiseIpv4: - bgp: - optionType: default - value: false - ospf: - optionType: default - value: false - ospfv3: - optionType: default - value: false - connected: - optionType: default - value: true - static: - optionType: default - value: true - eigrp: - optionType: default - value: false - lisp: - optionType: default - value: false - isis: - optionType: default - value: false - advertiseIpv6: - bgp: - optionType: default - value: false - ospf: - optionType: default - value: false - static: - optionType: default - value: false - eigrp: - optionType: default - value: false - connected: - optionType: default - value: false - lisp: - optionType: default - value: false - isis: - optionType: default - value: false - ignoreRegionPathLength: - optionType: default - value: false - transportGateway: - optionType: default - siteTypesForTransportGateway: +type: omp +config: + name: OMP + description: OMP Description + data: + gracefulRestart: + optionType: default + value: true + sendPathLimit: + optionType: default + value: 4 + ecmpLimit: + optionType: default + value: 4 + advertisementInterval: + optionType: default + value: 1 + holdtime: + optionType: default + value: 60 + eorTimer: + optionType: default + value: 300 + overlayAs: + optionType: default + shutdown: + optionType: default + value: false + gracefulRestartTimer: + optionType: default + value: 43200 + ompAdminDistanceIpv4: + optionType: default + value: 251 + ompAdminDistanceIpv6: + optionType: default + value: 251 + advertiseIpv4: + bgp: + optionType: default + value: false + ospf: + optionType: default + value: false + ospfv3: + optionType: default + value: false + connected: + optionType: default + value: true + static: + optionType: default + value: true + eigrp: + optionType: default + value: false + lisp: + optionType: default + value: false + isis: + optionType: default + value: false + advertiseIpv6: + bgp: + optionType: default + value: false + ospf: + optionType: default + value: false + static: + optionType: default + value: false + eigrp: + optionType: default + value: false + connected: + optionType: default + value: false + lisp: + optionType: default + value: false + isis: + optionType: default + value: false + ignoreRegionPathLength: + optionType: default + value: false + transportGateway: + optionType: default + siteTypesForTransportGateway: optionType: default \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/transport_parcels/ethernet.yml b/roles/feature_profile_builder/templates/transport_parcels/ethernet.yml index 2008bed..75691d4 100644 --- a/roles/feature_profile_builder/templates/transport_parcels/ethernet.yml +++ b/roles/feature_profile_builder/templates/transport_parcels/ethernet.yml @@ -1,150 +1,150 @@ -config: - data: - multiRegionFabric: - coreRegion: - optionType: default - value: core-shared - enableCoreRegion: - optionType: default - value: false - enableSecondaryRegion: - optionType: default - value: false - secondaryRegion: - optionType: default - value: secondary-shared - shutdown: - optionType: global - value: false - interfaceName: - optionType: variable - value: "{{ '{{if_name}}' }}" - description: - optionType: global - value: Interface - blockNonSourceIp: - optionType: default - value: false - autoDetectBandwidth: - optionType: default - value: false - tunnelInterface: - optionType: global - value: true - tunnel: - perTunnelQos: - optionType: default - value: false - color: - optionType: global - value: mpls - restrict: - optionType: default - value: false - group: - optionType: default - border: - optionType: default - value: false - maxControlConnections: - optionType: default - vBondAsStunServer: - optionType: default - value: false - excludeControllerGroupList: - optionType: default - vManageConnectionPreference: - optionType: default - value: 5 - portHop: - optionType: default - value: true - lowBandwidthLink: - optionType: default - value: false - tunnelTcpMss: - optionType: default - clearDontFragment: - optionType: default - value: false - ctsSgtPropagation: - optionType: default - value: false - networkBroadcast: - optionType: default - value: false - allowService: - all: - optionType: default - value: false - bgp: - optionType: default - value: false - dhcp: - optionType: default - value: true - ntp: - optionType: default - value: true - ssh: - optionType: default - value: true - dns: - optionType: default - value: true - icmp: - optionType: default - value: true - https: - optionType: default - value: true - ospf: - optionType: default - value: false - stun: - optionType: default - value: false - snmp: - optionType: default - value: false - netconf: - optionType: default - value: false - bfd: - optionType: default - value: false - nat: - optionType: default - value: false - intfIpAddress: - dynamic: - dynamicDhcpDistance: - optionType: default - value: 1 - advanced: - ipMtu: - optionType: default - value: 1500 - arpTimeout: - optionType: default - value: 1200 - loadInterval: - optionType: default - value: 30 - ipDirectedBroadcast: - optionType: default - value: false - icmpRedirectDisable: - optionType: default - value: true - encapsulation: - - encap: - optionType: global - value: ipsec - weight: - optionType: default - value: 1 - preference: - optionType: default - name: Ethernet_Parcel +config: + data: + multiRegionFabric: + coreRegion: + optionType: default + value: core-shared + enableCoreRegion: + optionType: default + value: false + enableSecondaryRegion: + optionType: default + value: false + secondaryRegion: + optionType: default + value: secondary-shared + shutdown: + optionType: global + value: false + interfaceName: + optionType: variable + value: "{{ '{{if_name}}' }}" + description: + optionType: global + value: Interface + blockNonSourceIp: + optionType: default + value: false + autoDetectBandwidth: + optionType: default + value: false + tunnelInterface: + optionType: global + value: true + tunnel: + perTunnelQos: + optionType: default + value: false + color: + optionType: global + value: mpls + restrict: + optionType: default + value: false + group: + optionType: default + border: + optionType: default + value: false + maxControlConnections: + optionType: default + vBondAsStunServer: + optionType: default + value: false + excludeControllerGroupList: + optionType: default + vManageConnectionPreference: + optionType: default + value: 5 + portHop: + optionType: default + value: true + lowBandwidthLink: + optionType: default + value: false + tunnelTcpMss: + optionType: default + clearDontFragment: + optionType: default + value: false + ctsSgtPropagation: + optionType: default + value: false + networkBroadcast: + optionType: default + value: false + allowService: + all: + optionType: default + value: false + bgp: + optionType: default + value: false + dhcp: + optionType: default + value: true + ntp: + optionType: default + value: true + ssh: + optionType: default + value: true + dns: + optionType: default + value: true + icmp: + optionType: default + value: true + https: + optionType: default + value: true + ospf: + optionType: default + value: false + stun: + optionType: default + value: false + snmp: + optionType: default + value: false + netconf: + optionType: default + value: false + bfd: + optionType: default + value: false + nat: + optionType: default + value: false + intfIpAddress: + dynamic: + dynamicDhcpDistance: + optionType: default + value: 1 + advanced: + ipMtu: + optionType: default + value: 1500 + arpTimeout: + optionType: default + value: 1200 + loadInterval: + optionType: default + value: 30 + ipDirectedBroadcast: + optionType: default + value: false + icmpRedirectDisable: + optionType: default + value: true + encapsulation: + - encap: + optionType: global + value: ipsec + weight: + optionType: default + value: 1 + preference: + optionType: default + name: Ethernet_Parcel description: Ethernet Parcel \ No newline at end of file diff --git a/roles/feature_profile_builder/templates/transport_parcels/vpn.yml b/roles/feature_profile_builder/templates/transport_parcels/vpn.yml index f32e6b8..8efc341 100644 --- a/roles/feature_profile_builder/templates/transport_parcels/vpn.yml +++ b/roles/feature_profile_builder/templates/transport_parcels/vpn.yml @@ -1,12 +1,12 @@ -type: vpn -config: - name: VPN_Parcel - description: VPN Parcel - data: - vpnId: - optionType: default - value: 0 - enhanceEcmpKeying: - optionType: global - value: true +type: vpn +config: + name: VPN_Parcel + description: VPN Parcel + data: + vpnId: + optionType: default + value: 0 + enhanceEcmpKeying: + optionType: global + value: true ipv4Route: [] \ No newline at end of file diff --git a/roles/software_upgrades/README.md b/roles/software_upgrades/README.md index da20ab3..9d31d38 100644 --- a/roles/software_upgrades/README.md +++ b/roles/software_upgrades/README.md @@ -25,6 +25,9 @@ The `software_upgrades` role performs the following tasks: - `cisco.catalystwan` collection installed. - Access details for the Cisco Manager instance must be provided. +- Run `playbooks/tests/test_manager_release_compatibility.yml` against a new + Manager train before performing a software upgrade. The current compatibility + target is Cisco Catalyst SD-WAN Manager 26.1. ## Dependencies @@ -78,7 +81,8 @@ Including an example of how to use your role (with variables passed in as parame ## Known Limitations -- The role assumes that controllers are <20.13 version. +- Upgrade endpoints are version-agnostic, but a live compatibility smoke test + is required for each newly published Manager train. - When directly uploading images from local machine to vManage, upload of a single image must complete within Server Session Timeout ## License diff --git a/roles/software_upgrades/tasks/main.yml b/roles/software_upgrades/tasks/main.yml index b763f27..0d2b5a9 100644 --- a/roles/software_upgrades/tasks/main.yml +++ b/roles/software_upgrades/tasks/main.yml @@ -4,7 +4,6 @@ --- # This role provides full workflow for upgrading devices with Remote Server. -# It assumes that controllers are <20.13 version # Currently doesn't include upgrades of cEdges from Remote Server. Known bug on vManage. - name: "Verify required variables for selected role" diff --git a/roles/vmanage_version/meta/main.yml b/roles/vmanage_version/meta/main.yml index 151463c..0af3681 100644 --- a/roles/vmanage_version/meta/main.yml +++ b/roles/vmanage_version/meta/main.yml @@ -4,7 +4,7 @@ galaxy_info: author: Przemyslaw Susko description: Check if vManage version matches requirements. license: GPL-3.0-or-later - min_ansible_version: "0.3.2" + min_ansible_version: "2.16.6" galaxy_tags: - cisco diff --git a/tests/sanity/ignore-2.21.txt b/tests/sanity/ignore-2.21.txt new file mode 100644 index 0000000..f097058 --- /dev/null +++ b/tests/sanity/ignore-2.21.txt @@ -0,0 +1,134 @@ +plugins/module_utils/filters.py import-3.12 +plugins/module_utils/filters.py import-3.13 +plugins/module_utils/filters.py import-3.14 +plugins/module_utils/policy_templates/definition.py import-3.12 +plugins/module_utils/policy_templates/definition.py import-3.13 +plugins/module_utils/policy_templates/definition.py import-3.14 +plugins/module_utils/policy_templates/list.py import-3.12 +plugins/module_utils/policy_templates/list.py import-3.13 +plugins/module_utils/policy_templates/list.py import-3.14 +plugins/module_utils/result.py import-3.12 +plugins/module_utils/result.py import-3.13 +plugins/module_utils/result.py import-3.14 +plugins/module_utils/vmanage_module.py import-3.12 +plugins/module_utils/vmanage_module.py import-3.13 +plugins/module_utils/vmanage_module.py import-3.14 +plugins/modules/active_sessions_info.py import-3.12 +plugins/modules/active_sessions_info.py import-3.13 +plugins/modules/active_sessions_info.py import-3.14 +plugins/modules/active_sessions_info.py validate-modules:import-error +plugins/modules/administration_settings.py import-3.12 +plugins/modules/administration_settings.py import-3.13 +plugins/modules/administration_settings.py import-3.14 +plugins/modules/administration_settings.py validate-modules:import-error +plugins/modules/alarms.py import-3.12 +plugins/modules/alarms.py import-3.13 +plugins/modules/alarms.py import-3.14 +plugins/modules/alarms.py validate-modules:import-error +plugins/modules/cli_templates.py import-3.12 +plugins/modules/cli_templates.py import-3.13 +plugins/modules/cli_templates.py import-3.14 +plugins/modules/cli_templates.py validate-modules:import-error +plugins/modules/cli_templates_info.py import-3.12 +plugins/modules/cli_templates_info.py import-3.13 +plugins/modules/cli_templates_info.py import-3.14 +plugins/modules/cli_templates_info.py validate-modules:import-error +plugins/modules/cluster_management.py import-3.12 +plugins/modules/cluster_management.py import-3.13 +plugins/modules/cluster_management.py import-3.14 +plugins/modules/cluster_management.py validate-modules:import-error +plugins/modules/config_group_deployment.py import-3.12 +plugins/modules/config_group_deployment.py import-3.13 +plugins/modules/config_group_deployment.py import-3.14 +plugins/modules/config_group_deployment.py validate-modules:import-error +plugins/modules/config_groups.py import-3.12 +plugins/modules/config_groups.py import-3.13 +plugins/modules/config_groups.py import-3.14 +plugins/modules/config_groups.py validate-modules:import-error +plugins/modules/device_templates.py import-3.12 +plugins/modules/device_templates.py import-3.13 +plugins/modules/device_templates.py import-3.14 +plugins/modules/device_templates.py validate-modules:import-error +plugins/modules/device_templates_info.py import-3.12 +plugins/modules/device_templates_info.py import-3.13 +plugins/modules/device_templates_info.py import-3.14 +plugins/modules/device_templates_info.py validate-modules:import-error +plugins/modules/device_templates_recovery.py import-3.12 +plugins/modules/device_templates_recovery.py import-3.13 +plugins/modules/device_templates_recovery.py import-3.14 +plugins/modules/device_templates_recovery.py validate-modules:import-error +plugins/modules/devices_certificates.py import-3.12 +plugins/modules/devices_certificates.py import-3.13 +plugins/modules/devices_certificates.py import-3.14 +plugins/modules/devices_certificates.py validate-modules:import-error +plugins/modules/devices_controllers.py import-3.12 +plugins/modules/devices_controllers.py import-3.13 +plugins/modules/devices_controllers.py import-3.14 +plugins/modules/devices_controllers.py validate-modules:import-error +plugins/modules/devices_info.py import-3.12 +plugins/modules/devices_info.py import-3.13 +plugins/modules/devices_info.py import-3.14 +plugins/modules/devices_info.py validate-modules:import-error +plugins/modules/devices_wan_edges.py import-3.12 +plugins/modules/devices_wan_edges.py import-3.13 +plugins/modules/devices_wan_edges.py import-3.14 +plugins/modules/devices_wan_edges.py validate-modules:import-error +plugins/modules/feature_profile_builder.py import-3.12 +plugins/modules/feature_profile_builder.py import-3.13 +plugins/modules/feature_profile_builder.py import-3.14 +plugins/modules/feature_templates.py import-3.12 +plugins/modules/feature_templates.py import-3.13 +plugins/modules/feature_templates.py import-3.14 +plugins/modules/feature_templates.py validate-modules:import-error +plugins/modules/feature_templates_info.py import-3.12 +plugins/modules/feature_templates_info.py import-3.13 +plugins/modules/feature_templates_info.py import-3.14 +plugins/modules/feature_templates_info.py validate-modules:import-error +plugins/modules/health_checks.py import-3.12 +plugins/modules/health_checks.py import-3.13 +plugins/modules/health_checks.py import-3.14 +plugins/modules/health_checks.py validate-modules:import-error +plugins/modules/policy.py import-3.12 +plugins/modules/policy.py import-3.13 +plugins/modules/policy.py import-3.14 +plugins/modules/policy.py validate-modules:import-error +plugins/modules/server_info.py import-3.12 +plugins/modules/server_info.py import-3.13 +plugins/modules/server_info.py import-3.14 +plugins/modules/server_info.py validate-modules:import-error +plugins/modules/software_repository.py import-3.12 +plugins/modules/software_repository.py import-3.13 +plugins/modules/software_repository.py import-3.14 +plugins/modules/software_repository.py validate-modules:import-error +plugins/modules/software_repository_info.py import-3.12 +plugins/modules/software_repository_info.py import-3.13 +plugins/modules/software_repository_info.py import-3.14 +plugins/modules/software_repository_info.py validate-modules:import-error +plugins/modules/software_upgrade.py import-3.12 +plugins/modules/software_upgrade.py import-3.13 +plugins/modules/software_upgrade.py import-3.14 +plugins/modules/software_upgrade.py validate-modules:import-error +plugins/modules/software_upgrade_info.py import-3.12 +plugins/modules/software_upgrade_info.py import-3.13 +plugins/modules/software_upgrade_info.py import-3.14 +plugins/modules/software_upgrade_info.py validate-modules:import-error +plugins/modules/tenants.py import-3.12 +plugins/modules/tenants.py import-3.13 +plugins/modules/tenants.py import-3.14 +plugins/modules/tenants.py validate-modules:import-error +plugins/modules/tenants_info.py import-3.12 +plugins/modules/tenants_info.py import-3.13 +plugins/modules/tenants_info.py import-3.14 +plugins/modules/tenants_info.py validate-modules:import-error +plugins/modules/users.py import-3.12 +plugins/modules/users.py import-3.13 +plugins/modules/users.py import-3.14 +plugins/modules/users.py validate-modules:import-error +plugins/modules/vmanage_mode.py import-3.12 +plugins/modules/vmanage_mode.py import-3.13 +plugins/modules/vmanage_mode.py import-3.14 +plugins/modules/vmanage_mode.py validate-modules:import-error +plugins/modules/wait_for_api_server.py import-3.12 +plugins/modules/wait_for_api_server.py import-3.13 +plugins/modules/wait_for_api_server.py import-3.14 +plugins/modules/wait_for_api_server.py validate-modules:import-error diff --git a/tests/unit/test_sdk_compatibility.py b/tests/unit/test_sdk_compatibility.py new file mode 100644 index 0000000..1c80477 --- /dev/null +++ b/tests/unit/test_sdk_compatibility.py @@ -0,0 +1,30 @@ +import importlib +from pathlib import Path + +from catalystwan.version import parse_api_version + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] + + +def test_all_collection_modules_import_with_installed_sdk(): + module_paths = sorted((REPOSITORY_ROOT / "plugins" / "modules").glob("*.py")) + + for module_path in module_paths: + if module_path.name == "__init__.py": + continue + importlib.import_module(f"plugins.modules.{module_path.stem}") + + +def test_current_manager_release_version_is_parsed(): + assert str(parse_api_version("26.1.1")) == "26.1" + assert str(parse_api_version("26.1.2-123")) == "26.1" + + +def test_result_models_do_not_share_mutable_defaults(): + from plugins.module_utils.result import ModuleResult + + first = ModuleResult() + second = ModuleResult() + first.response["changed"] = True + + assert second.response == {} diff --git a/tests/unit/test_software_upgrade.py b/tests/unit/test_software_upgrade.py new file mode 100644 index 0000000..1c97df4 --- /dev/null +++ b/tests/unit/test_software_upgrade.py @@ -0,0 +1,21 @@ +from types import SimpleNamespace + +from plugins.modules.software_upgrade import _all_devices_are_managers + + +def test_all_devices_are_managers_accepts_manager_only_selection(): + devices = [SimpleNamespace(personality="vmanage"), SimpleNamespace(personality="vmanage")] + + assert _all_devices_are_managers(devices) + + +def test_all_devices_are_managers_rejects_mixed_selection(): + devices = [SimpleNamespace(personality="vmanage"), SimpleNamespace(personality="vedge")] + + assert not _all_devices_are_managers(devices) + + +def test_all_devices_are_managers_rejects_edge_only_selection(): + devices = [SimpleNamespace(personality="vedge")] + + assert not _all_devices_are_managers(devices) diff --git a/utils/docs_fragments_template.j2 b/utils/docs_fragments_template.j2 index c9167c4..a071b5e 100644 --- a/utils/docs_fragments_template.j2 +++ b/utils/docs_fragments_template.j2 @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/utils/ft_device_model.j2 b/utils/ft_device_model.j2 index 4a6567a..4c3cdc0 100644 --- a/utils/ft_device_model.j2 +++ b/utils/ft_device_model.j2 @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Cisco Systems, Inc. and its affiliates diff --git a/utils/ft_generator.py b/utils/ft_generator.py index c537dbd..e217142 100644 --- a/utils/ft_generator.py +++ b/utils/ft_generator.py @@ -49,7 +49,7 @@ def field_to_ansible_option(field: FieldInfo, field_name: str, model_name: str): option["default"] = field.default elif safe_issubclass(type(field.default), list): option["default"] = field.default - elif type(field.default) is DeviceVariable: + elif isinstance(field.default, DeviceVariable): option["default"] = field.default.name field_type = get_origin(field.annotation) or field.annotation @@ -113,11 +113,11 @@ def field_to_ansible_option(field: FieldInfo, field_name: str, model_name: str): elif get_origin(user_class) == Literal: option["type"] = "list" option["elements"] = "str" - option["choices"] = [item for item in get_args(user_class)] + option["choices"] = list(get_args(user_class)) elif origin_type == Literal: option["type"] = "list" option["elements"] = "str" - option["choices"] = [item for item in get_args(elements_type)] + option["choices"] = list(get_args(elements_type)) elif user_class == int: option["type"] = "list" option["elements"] = "int" @@ -161,11 +161,11 @@ def field_to_ansible_option(field: FieldInfo, field_name: str, model_name: str): elif field_type == Union and Literal in subargs_base_types: elements_type = next((arg for arg in args if arg is not None), None) option["type"] = "str" - option["choices"] = [item for item in get_args(elements_type)] + option["choices"] = list(get_args(elements_type)) elif field_type == Literal: option["type"] = "str" - option["choices"] = [item for item in args] + option["choices"] = list(args) else: option["type"] = "str" @@ -296,12 +296,10 @@ def to_nice_yaml(data): print(f"File '{file_name}' has been written successfully.") -print( - """ +print(""" When used, note that Device Specific Variables doesn't have description and it required manual effort to fix these in documentation. Example: cisco.catalystwan.feature_template_cisco_system requires updating few fields. Look for '- null' fields. That will be solved once we will have Device Specific Variables in SDK properly defined. - """ -) + """)