From 154cc160f9345c96b01eef7807cb5910bfab14bc Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 6 Feb 2024 14:50:01 +0530 Subject: [PATCH 01/64] Added new workflow modules and playbooks --- .../device_credential_workflow_manager.yml | 119 + .../network_settings_workflow_manager.yml | 111 + playbooks/template_workflow_manager.yml | 37 + .../device_credential_workflow_manager.py | 2615 +++++++++++++++ .../network_settings_workflow_manager.py | 2160 +++++++++++++ plugins/modules/template_workflow_manager.py | 2793 +++++++++++++++++ 6 files changed, 7835 insertions(+) create mode 100644 playbooks/device_credential_workflow_manager.yml create mode 100644 playbooks/network_settings_workflow_manager.yml create mode 100644 playbooks/template_workflow_manager.yml create mode 100644 plugins/modules/device_credential_workflow_manager.py create mode 100644 plugins/modules/network_settings_workflow_manager.py create mode 100644 plugins/modules/template_workflow_manager.py diff --git a/playbooks/device_credential_workflow_manager.yml b/playbooks/device_credential_workflow_manager.yml new file mode 100644 index 0000000000..2955ec64d1 --- /dev/null +++ b/playbooks/device_credential_workflow_manager.yml @@ -0,0 +1,119 @@ +- hosts: dnac_servers + vars_files: + - credentials.yml + gather_facts: no + connection: local + tasks: +# +# Project Info Section +# + + - name: Create Credentials and assign it to a site. + cisco.dnac.device_credential_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + state: merged + config: + - global_credential_details: + cli_credential: + - description: CLI1 + username: cli1 + password: '12345' + enable_password: '12345' + # old_description: + # old_username: + # id: e448ea13-4de0-406b-bc6e-f72b57ed6746 # Use this for updation or deletion + snmp_v2c_read: + - description: SNMPv2c Read1 # use this for deletion + read_community: '123456' + # old_description: # use this for updating the description + # id: 0ee7d677-8804-43f2-8b6c-599c5f18348f # Use this for updation or deletion + snmp_v2c_write: + - description: SNMPv2c Write1 # use this for deletion + write_community: '123456' + # old_description: # use this for updating the description + # id: a96abc1b-1fd6-41f1-8a6d-a5569c17262d # Use this for updation or deletion + snmp_v3: + - auth_password: '12345678' # Atleast 8 characters + auth_type: SHA # [SHA, MD5] (SHA is recommended) + snmp_mode: AUTHPRIV # [AUTHPRIV, AUTHNOPRIV, NOAUTHNOPRIV] + privacy_password: '12345678' # Atleast 8 characters + privacy_type: AES128 # [AE128, AE192, AE256] + username: snmpV31 + description: snmpV31 + # old_description: + # id: d8974823-250a-41b0-8c9b-b27b2ae01472 # Use this for updation or deletion + https_read: + - description: HTTP Read1 + username: HTTP_Read1 + password: '12345' + port: 443 + # old_description: + # old_username: + # id: a7ef9995-e404-4240-94ca-b5f37f65c19d # Use this for updation or deletion + https_write: + - description: HTTP Write1 + username: HTTP_Write1 + password: '12345' + port: 443 + # old_description: + # old_username: + # id: bec9818e-30cd-468b-bf75-292beefc2e20 # Use this for updation or deletion + assign_credentials_to_site: + cli_credential: + # description: CLI + # username: cli + id: 2fc5f7d4-cf15-4a4f-99b3-f086e8dd6350 + snmp_v2c_read: + # description: SNMPv2c Read + id: a966a4e5-9d11-4683-8edc-a5ad8fa59ee3 + snmp_v2c_write: + # description: SNMPv2c Write + id: 7cd072a4-2263-4087-b6ec-93b20958e286 + snmp_v3: + # description: snmpV3 + id: c08a1797-84ce-4add-94a3-b419b13621e4 + https_read: + # description: HTTP Read + # username: HTTP_Read + id: 1009725d-373b-4e7c-a091-300777e2bbe2 + https_write: + # description: HTTP Write + # username: HTTP_Write + id: f1ab6e3d-01e9-4d87-8271-3ac5fde83980 + site_name: + - Global/Chennai/Trill + - Global/Chennai/Tidel + + - name: Delete Credentials + cisco.dnac.device_credential_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + state: deleted + config: + - global_credential_details: + cli_credential: + - description: CLI1 + username: cli1 + snmp_v2c_read: + - description: SNMPv2c Read1 # use this for deletion + snmp_v2c_write: + - description: SNMPv2c Write1 # use this for deletion + snmp_v3: + - description: snmpV31 + https_read: + - description: HTTP Read1 + username: HTTP_Read1 + https_write: + - description: HTTP Write1 + username: HTTP_Write1 diff --git a/playbooks/network_settings_workflow_manager.yml b/playbooks/network_settings_workflow_manager.yml new file mode 100644 index 0000000000..40fb93c29b --- /dev/null +++ b/playbooks/network_settings_workflow_manager.yml @@ -0,0 +1,111 @@ +- hosts: dnac_servers + vars_files: + - credentials.yml + gather_facts: no + connection: local + tasks: +# +# Project Info Section +# + + - name: Create global pool, reserve subpool and network functions + cisco.dnac.network_settings_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + dnac_log_append: True + dnac_log_file_path: "{{ dnac_log_file_path }}" + state: merged + config_verify: True + config: + - global_pool_details: + settings: + ip_pool: + - name: Global_Pool2 + gateway: '' #use this for updating + ip_address_space: IPv6 #required when we are creating + cidr: 2001:db8::/64 #required when we are creating + type: Generic + dhcp_server_ips: [] #use this for updating + dns_server_ips: [] #use this for updating + # prev_name: Global_Pool2 + reserve_pool_details: + ipv6_address_space: True + ipv4_global_pool: 100.0.0.0/8 + ipv4_prefix: True + ipv4_prefix_length: 9 + ipv4_subnet: 100.128.0.0 + # ipv4_dns_servers: [100.128.0.1] + name: IP_Pool_3 + ipv6_prefix: True + ipv6_prefix_length: 64 + ipv6_global_pool: 2001:db8::/64 + ipv6_subnet: '2001:db8::' + site_name: Global/Chennai/Trill + slaac_support: True + # prev_name: IP_Pool_4 + type: LAN + network_management_details: + settings: + dhcp_server: + - 10.0.0.1 + dns_server: + domain_name: cisco.com + primary_ip_address: 10.0.0.2 + secondary_ip_address: 10.0.0.3 + client_and_endpoint_aaa: #works only if we system settigns is set + ip_address: 10.197.156.42 #Mandatory for ISE, sec ip for AAA + network: 10.0.0.20 + protocol: RADIUS + servers: AAA + # shared_secret: string #ISE + message_of_the_day: + banner_message: hello + retain_existing_banner: 'true' + netflow_collector: + ip_address: 10.0.0.4 + port: 443 + network_aaa: #works only if we system settigns is set + ip_address: 10.0.0.21 #Mandatory for ISE, sec ip for AAA + network: 10.0.0.20 + protocol: TACACS + servers: AAA + # shared_secret: string #ISE + ntp_server: + - 10.0.0.5 + snmp_server: + configure_dnac_ip: false + # ip_addresses: + # - 10.0.0.6 + syslog_server: + configure_dnac_ip: false + # ip_addresses: + # - 10.0.0.7 + timezone: GMT + site_name: Global/Chennai + + - name: Delete Global Pool and Release Pool Reservation + cisco.dnac.network_settings_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: deleted + config_verify: True + config: + - global_pool_details: + settings: + ip_pool: + - name: Global_Pool2 + reserve_pool_details: + name: IP_Pool_3 + site_name: Global/Chennai/Trill diff --git a/playbooks/template_workflow_manager.yml b/playbooks/template_workflow_manager.yml new file mode 100644 index 0000000000..35a7a60d2d --- /dev/null +++ b/playbooks/template_workflow_manager.yml @@ -0,0 +1,37 @@ +- hosts: localhost + vars_files: + - credentials.yml + - device_details.template + gather_facts: false + connection: local + tasks: +# +# Project Info Section +# + - name: Test project template + cisco.dnac.template_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: true + state: "merged" + config_verify: true + #ignore_errors: true #Enable this to continue execution even the task fails + config: + - configuration_templates: + project_name: "{{ item.proj_name }}" + template_name: "{{ item.temp_name }}" + template_content: "{{ item.device_config }}" + version_description: "{{ item.description }}" + language: "{{ item.language }}" + software_type: "{{ item.type }}" + software_variant: "{{ item.variant }}" + device_types: + - product_family: "{{ item.family }}" + register: template_result + with_items: '{{ template_details }}' + tags: + - template diff --git a/plugins/modules/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py new file mode 100644 index 0000000000..cd5b44cae3 --- /dev/null +++ b/plugins/modules/device_credential_workflow_manager.py @@ -0,0 +1,2615 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2023, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Ansible module to perform operations on device credentials in Cisco Catalyst Center.""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ['Muthu Rakesh, Madhan Sankaranarayanan'] + +DOCUMENTATION = r""" +--- +module: device_credential_workflow_manager +short_description: Resource module for Global Device Credentials and Assigning Credentials to sites. +description: +- Manage operations on Global Device Credentials and Assigning Credentials to sites. +- API to create global device credentials. +- API to update global device credentials. +- API to delete global device credentials. +- API to assign the device credential to the site. +version_added: '6.7.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Muthu Rakesh (@MUTHU-RAKESH-27) + Madhan Sankaranarayanan (@madhansansel) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center after applying the playbook config. + type: bool + default: False + state: + description: The state of Cisco Catalyst Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of global device credentials and site names. + type: list + elements: dict + required: true + suboptions: + global_credential_details: + description: Manages global device credentials + type: dict + suboptions: + cli_credential: + description: Global Credential V2's cliCredential. + type: list + elements: dict + suboptions: + description: + description: Description. Required for creating the credential. + type: str + enable_password: + description: + - cli_credential credential Enable Password. + - Password cannot contain spaces or angle brackets (< >) + type: str + id: + description: Credential Id. Use this for updating the device credential. + type: str + password: + description: + - cli_credential credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). + type: str + username: + description: + - cli_credential credential Username. + - Username cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description/Username. + type: str + old_username: + description: Old Username. Use this for updating the description/Username. + type: str + https_read: + description: Global Credential V2's httpsRead. + type: list + elements: dict + suboptions: + id: + description: Credential Id. Use this for updating the device credential. + type: str + name: + description: Name. Required for creating the credential. + type: str + password: + description: + - https_read credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). + type: str + port: + description: Port. Default port is 443. + type: int + username: + description: + - https_read credential Username. + - Username cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description/Username. + type: str + old_username: + description: Old Username. Use this for updating the description/Username. + type: str + https_write: + description: Global Credential V2's httpsWrite. + type: list + elements: dict + suboptions: + id: + description: Credential Id. Use this for updating the device credential. + type: str + name: + description: Name. Required for creating the credential. + type: str + password: + description: + - https_write credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). + type: str + port: + description: Port. Default port is 443. + type: int + username: + description: + - https_write credential Username. + - Username cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description/Username. + type: str + old_username: + description: Old Username. Use this for updating the description/Username. + type: str + snmp_v2c_read: + description: Global Credential V2's snmpV2cRead. + type: list + elements: dict + suboptions: + description: + description: Description. Required for creating the credential. + type: str + id: + description: Credential Id. Use this for updating the device credential. + type: str + read_community: + description: + - snmp_v2c_read Read Community. + - Password cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description. + type: str + snmp_v2c_write: + description: Global Credential V2's snmpV2cWrite. + type: list + elements: dict + suboptions: + description: + description: Description. Required for creating the credential. + type: str + id: + description: Credential Id. Use this for updating the device credential. + type: str + write_community: + description: + - snmp_v2c_write Write Community. + - Password cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description. + type: str + snmp_v3: + description: Global Credential V2's snmpV3. + type: list + elements: dict + suboptions: + auth_password: + description: + - snmp_v3 Auth Password. + - Password must contain minimum 8 characters. + - Password cannot contain spaces or angle brackets (< >). + type: str + auth_type: + description: Auth Type. ["SHA", "MD5"]. + type: str + description: + description: + - snmp_v3 Description. + - Should be unique from other snmp_v3 credentials. + type: str + id: + description: Credential Id. Use this for updating the device credential. + type: str + privacy_password: + description: + - snmp_v3 Privacy Password. + - Password must contain minimum 8 characters. + - Password cannot contain spaces or angle brackets (< >). + type: str + privacy_type: + description: Privacy Type. ["AES128", "AES192", "AES256"]. + type: str + snmp_mode: + description: Snmp Mode. ["AUTHPRIV", "AUTHNOPRIV", "NOAUTHNOPRIV"]. + type: str + username: + description: + - snmp_v3 credential Username. + - Username cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description. + type: str + assign_credentials_to_site: + description: Assign Device Credentials to Site. + type: dict + suboptions: + cli_credential: + description: CLI Credential. + type: dict + suboptions: + description: + description: CLI Credential Description. + type: str + username: + description: CLI Credential Username. + type: str + id: + description: CLI Credential Id. Use (Description, Username) or Id. + type: str + https_read: + description: HTTP(S) Read Credential + type: dict + suboptions: + description: + description: HTTP(S) Read Credential Description. + type: str + username: + description: HTTP(S) Read Credential Username. + type: str + id: + description: HTTP(S) Read Credential Id. Use (Description, Username) or Id. + type: str + https_write: + description: HTTP(S) Write Credential + type: dict + suboptions: + description: + description: HTTP(S) Write Credential Description. + type: str + username: + description: HTTP(S) Write Credential Username. + type: str + id: + description: HTTP(S) Write Credential Id. Use (Description, Username) or Id. + type: str + site_name: + description: Site Name to assign credential. + type: list + elements: str + snmp_v2c_read: + description: SNMPv2c Read Credential + type: dict + suboptions: + description: + description: SNMPv2c Read Credential Description. + type: str + id: + description: SNMPv2c Read Credential Id. Use Description or Id. + type: str + snmp_v2c_write: + description: SNMPv2c Write Credential + type: dict + suboptions: + description: + description: SNMPv2c Write Credential Description. + type: str + id: + description: SNMPv2c Write Credential Id. Use Description or Id. + type: str + snmp_v3: + description: snmp_v3 Credential + type: dict + suboptions: + description: + description: snmp_v3 Credential Description. + type: str + id: + description: snmp_v3 Credential Id. Use Description or Id. + type: str +requirements: +- dnacentersdk >= 2.5.5 +- python >= 3.5 +seealso: +- name: Cisco Catalyst Center documentation for Discovery CreateGlobalCredentialsV2 + description: Complete reference of the CreateGlobalCredentialsV2 API. + link: https://developer.cisco.com/docs/dna-center/#!create-global-credentials-v-2 +- name: Cisco Catalyst Center documentation for Discovery DeleteGlobalCredentialV2 + description: Complete reference of the DeleteGlobalCredentialV2 API. + link: https://developer.cisco.com/docs/dna-center/#!delete-global-credential-v-2 +- name: Cisco Catalyst Center documentation for Discovery UpdateGlobalCredentialsV2 + description: Complete reference of the UpdateGlobalCredentialsV2 API. + link: https://developer.cisco.com/docs/dna-center/#!update-global-credentials-v-2 +- name: Cisco Catalyst Center documentation for Network Settings AssignDeviceCredentialToSiteV2 + description: Complete reference of the AssignDeviceCredentialToSiteV2 API. + link: https://developer.cisco.com/docs/dna-center/#!assign-device-credential-to-site-v-2 +notes: + - SDK Method used are + discovery.Discovery.create_global_credentials_v2, + discovery.Discovery.delete_global_credential_v2, + discovery.Discovery.update_global_credentials_v2, + network_settings.NetworkSettings.assign_device_credential_to_site_v2, + + - Paths used are + post /dna/intent/api/v2/global-credential, + delete /dna/intent/api/v2/global-credential/{id}, + put /dna/intent/api/v2/global-credential, + post /dna/intent/api/v2/credential-to-site/{siteId}, +""" + +EXAMPLES = r""" +--- + - name: Create Credentials and assign it to a site. + cisco.dnac.device_credential_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - global_credential_details: + cli_credential: + - description: string + username: string + password: string + enable_password: string + snmp_v2c_read: + - description: string + read_community: string + snmp_v2c_write: + - description: string + write_community: string + snmp_v3: + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 + username: string + description: string + https_read: + - description: string + username: string + password: string + port: 443 + https_write: + - description: string + username: string + password: string + port: 443 + assign_credentials_to_site: + cli_credential: + id: string + snmp_v2c_read: + id: string + snmp_v2c_write: + id: string + snmp_v3: + id: string + https_read: + id: string + https_write: + id: string + site_name: + - string + + - name: Create Multiple Credentials. + cisco.dnac.device_credential_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - global_credential_details: + cli_credential: + - description: string + username: string + password: string + enable_password: string + - description: string + username: string + password: string + enable_password: string + snmp_v2c_read: + - description: string + read_community: string + - description: string + read_community: string + snmp_v2c_write: + - description: string + write_community: string + - description: string + write_community: string + snmp_v3: + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 + username: string + description: string + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 + username: string + description: string + https_read: + - description: string + username: string + password: string + port: 443 + - description: string + username: string + password: string + port: 443 + https_write: + - description: string + username: string + password: string + port: 443 + - description: string + username: string + password: string + port: 443 + + - name: Update global device credentials using id + cisco.dnac.device_credential_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - global_credential_details: + cli_credential: + - description: string + username: string + password: string + enable_password: string + id: string + snmp_v2c_read: + - description: string + read_community: string + id: string + snmp_v2c_write: + - description: string + write_community: string + id: string + snmp_v3: + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 + username: string + description: string + id: string + https_read: + - description: string + username: string + password: string + port: 443 + id: string + https_write: + - description: string + username: string + password: string + port: 443 + id: string + + - name: Update multiple global device credentials using id + cisco.dnac.device_credential_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - global_credential_details: + cli_credential: + - description: string + username: string + password: string + enable_password: string + id: string + - description: string + username: string + password: string + enable_password: string + id: string + snmp_v2c_read: + - description: string + read_community: string + id: string + - description: string + read_community: string + id: string + snmp_v2c_write: + - description: string + write_community: string + id: string + - description: string + write_community: string + id: string + snmp_v3: + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 + username: string + description: string + id: string + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 + username: string + description: string + id: string + https_read: + - description: string + username: string + password: string + port: 443 + id: string + - description: string + username: string + password: string + port: 443 + id: string + https_write: + - description: string + username: string + password: string + port: 443 + id: string + - description: string + username: string + password: string + port: 443 + id: string + + - name: Update global device credential name/description using old name and description. + cisco.dnac.device_credential_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - global_credential_details: + cli_credential: + - description: string + username: string + password: string + enable_password: string + old_description: string + old_username: string + snmp_v2c_read: + - description: string + read_community: string + old_description: string + snmp_v2c_write: + - description: string + write_community: string + old_description: string + snmp_v3: + - auth_password: string + auth_type: string + snmp_mode: string + privacy_password: string + privacy_type: string + username: string + description: string + https_read: + - description: string + username: string + password: string + port: string + old_description: string + old_username: string + https_write: + - description: string + username: string + password: string + port: string + old_description: string + old_username: string + + - name: Assign Credentials to sites using old description and username. + cisco.dnac.device_credential_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - assign_credentials_to_site: + cli_credential: + description: string + username: string + snmp_v2c_read: + description: string + snmp_v2c_write: + description: string + snmp_v3: + description: string + https_read: + description: string + username: string + https_write: + description: string + username: string + site_name: + - string + - string + +""" + +RETURN = r""" +# Case_1: Successful creation/updation/deletion of global device credentials +dnac_response1: + description: A dictionary or list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": { + "taskId": "string", + "url": "string" + }, + "version": "string" + } + +# Case_2: Successful assignment of global device credentials to a site. +dnac_response2: + description: A dictionary or list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": { + "taskId": "string", + "url": "string" + }, + "version": "string" + } +""" + +import copy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, + get_dict_result, +) + + +class DeviceCredential(DnacBase): + """Class containing member attributes for device credential intent module""" + + def __init__(self, module): + super().__init__(module) + self.result["response"] = [ + { + "globalCredential": {}, + "assignCredential": {} + } + ] + + def validate_input(self): + """ + Validate the fields provided in the playbook. + Checks the configuration provided in the playbook against a predefined specification + to ensure it adheres to the expected structure and data types. + Parameters: + self: The instance of the class containing the 'config' attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of 'config' parameter. + Example: + To use this method, create an instance of the class and call 'validate_input' on it. + If the validation succeeds, 'self.status' will be 'success' and 'self.validated_config' + will contain the validated configuration. If it fails, 'self.status' will be 'failed', + 'self.msg' will describe the validation issues. + + """ + + if not self.config: + self.msg = "config not available in playbook for validation" + self.status = "success" + return self + + # temp_spec is the specification for the expected structure of configuration parameters + temp_spec = { + "global_credential_details": { + "type": 'dict', + "cli_credential": { + "type": 'list', + "description": {"type": 'string'}, + "username": {"type": 'string'}, + "password": {"type": 'string'}, + "enable_password": {"type": 'string'}, + "old_description": {"type": 'string'}, + "old_username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmp_v2c_read": { + "type": 'list', + "description": {"type": 'string'}, + "read_community": {"type": 'string'}, + "old_description": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmp_v2c_write": { + "type": 'list', + "description": {"type": 'string'}, + "write_community": {"type": 'string'}, + "old_description": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmp_v3": { + "type": 'list', + "description": {"type": 'string'}, + "username": {"type": 'string'}, + "snmp_mode": {"type": 'string'}, + "auth_type": {"type": 'string'}, + "auth_password": {"type": 'string'}, + "privacy_type": {"type": 'string'}, + "privacy_password": {"type": 'string'}, + "old_description": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "https_read": { + "type": 'list', + "description": {"type": 'string'}, + "username": {"type": 'string'}, + "password": {"type": 'string'}, + "port": {"type": 'integer'}, + "old_description": {"type": 'string'}, + "old_username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "https_write": { + "type": 'list', + "description": {"type": 'string'}, + "username": {"type": 'string'}, + "password": {"type": 'string'}, + "port": {"type": 'integer'}, + "old_description": {"type": 'string'}, + "old_username": {"type": 'string'}, + "id": {"type": 'string'}, + } + }, + "assign_credentials_to_site": { + "type": 'dict', + "cli_credential": { + "type": 'dict', + "description": {"type: 'string'"}, + "username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmp_v2c_read": { + "type": 'dict', + "description": {"type: 'string'"}, + "username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmp_v2c_write": { + "type": 'dict', + "description": {"type: 'string'"}, + "id": {"type": 'string'}, + }, + "snmp_v3": { + "type": 'dict', + "description": {"type: 'string'"}, + "id": {"type": 'string'}, + }, + "https_read": { + "type": 'dict', + "description": {"type: 'string'"}, + "username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "https_write": { + "type": 'dict', + "description": {"type: 'string'"}, + "username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "site_name": { + "type": 'list', + "elements": 'string' + } + } + } + + # Validate playbook params against the specification (temp_spec) + valid_temp, invalid_params = validate_list_of_dicts(self.config, temp_spec) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format("\n".join(invalid_params)) + self.status = "failed" + return self + + self.validated_config = valid_temp + self.log("Successfully validated playbook config params: {0}".format(valid_temp), "INFO") + self.msg = "Successfully validated input from the playbook" + self.status = "success" + return self + + def get_site_id(self, site_name): + """ + Get the site id from the site name. + Use check_return_status() to check for failure + + Parameters: + site_name (str) - Site name + + Returns: + str or None - The Site Id if found, or None if not found or error + """ + + try: + response = self.dnac._exec( + family="sites", + function='get_site', + params={"name": site_name}, + ) + self.log("Received API response from 'get_site': {0}".format(response), "DEBUG") + if not response: + self.log("Failed to retrieve the site ID for the site name: {0}" + .format(site_name), "ERROR") + return None + + _id = response.get("response")[0].get("id") + self.log("Site ID for the site name {0}: {1}".format(site_name, _id), "INFO") + except Exception as exec: + self.log("Exception occurred while getting site_id from the site_name: {0}" + .format(exec), "CRITICAL") + return None + + return _id + + def get_global_credentials_params(self): + """ + Get the current Global Device Credentials from Cisco Catalyst Center. + + Parameters: + self - The current object details. + + Returns: + global_credentials (dict) - All global device credentials details. + """ + + try: + global_credentials = self.dnac._exec( + family="discovery", + function='get_all_global_credentials_v2', + ) + global_credentials = global_credentials.get("response") + self.log("All global device credentials details: {0}" + .format(global_credentials), "DEBUG") + except Exception as exec: + self.log("Exception occurred while getting global device credentials: {0}" + .format(exec), "CRITICAL") + return None + + return global_credentials + + def get_cli_params(self, cliDetails): + """ + Format the CLI parameters for the CLI credential configuration in Cisco Catalyst Center. + + Parameters: + cliDetails (list of dict) - Cisco Catalyst Center details containing CLI Credentials. + + Returns: + cliCredential (list of dict) - Processed CLI credential data + in the format suitable for the Cisco Catalyst Center config. + """ + + cliCredential = [] + for item in cliDetails: + if item is None: + cliCredential.append(None) + else: + value = { + "username": item.get("username"), + "description": item.get("description"), + "id": item.get("id") + } + cliCredential.append(value) + return cliCredential + + def get_snmpV2cRead_params(self, snmpV2cReadDetails): + """ + Format the snmpV2cRead parameters for the snmpV2cRead + credential configuration in Cisco Catalyst Center. + + Parameters: + snmpV2cReadDetails (list of dict) - Cisco Catalyst Center + Details containing snmpV2cRead Credentials. + + Returns: + snmpV2cRead (list of dict) - Processed snmpV2cRead credential + data in the format suitable for the Cisco Catalyst Center config. + """ + + snmpV2cRead = [] + for item in snmpV2cReadDetails: + if item is None: + snmpV2cRead.append(None) + else: + value = { + "description": item.get("description"), + "id": item.get("id") + } + snmpV2cRead.append(value) + return snmpV2cRead + + def get_snmpV2cWrite_params(self, snmpV2cWriteDetails): + """ + Format the snmpV2cWrite parameters for the snmpV2cWrite + credential configuration in Cisco Catalyst Center. + + Parameters: + snmpV2cWriteDetails (list of dict) - Cisco Catalyst Center + Details containing snmpV2cWrite Credentials. + + Returns: + snmpV2cWrite (list of dict) - Processed snmpV2cWrite credential + data in the format suitable for the Cisco Catalyst Center config. + """ + + snmpV2cWrite = [] + for item in snmpV2cWriteDetails: + if item is None: + snmpV2cWrite.append(None) + else: + value = { + "description": item.get("description"), + "id": item.get("id") + } + snmpV2cWrite.append(value) + return snmpV2cWrite + + def get_httpsRead_params(self, httpsReadDetails): + """ + Format the httpsRead parameters for the httpsRead + credential configuration in Cisco Catalyst Center. + + Parameters: + httpsReadDetails (list of dict) - Cisco Catalyst Center + Details containing httpsRead Credentials. + + Returns: + httpsRead (list of dict) - Processed httpsRead credential + data in the format suitable for the Cisco Catalyst Center config. + """ + + httpsRead = [] + for item in httpsReadDetails: + if item is None: + httpsRead.append(None) + else: + value = { + "description": item.get("description"), + "username": item.get("username"), + "port": item.get("port"), + "id": item.get("id") + } + httpsRead.append(value) + return httpsRead + + def get_httpsWrite_params(self, httpsWriteDetails): + """ + Format the httpsWrite parameters for the httpsWrite + credential configuration in Cisco Catalyst Center. + + Parameters: + httpsWriteDetails (list of dict) - Cisco Catalyst Center + Details containing httpsWrite Credentials. + + Returns: + httpsWrite (list of dict) - Processed httpsWrite credential + data in the format suitable for the Cisco Catalyst Center config. + """ + + httpsWrite = [] + for item in httpsWriteDetails: + if item is None: + httpsWrite.append(None) + else: + value = { + "description": item.get("description"), + "username": item.get("username"), + "port": item.get("port"), + "id": item.get("id") + } + httpsWrite.append(value) + return httpsWrite + + def get_snmpV3_params(self, snmpV3Details): + """ + Format the snmpV3 parameters for the snmpV3 credential configuration in Cisco Catalyst Center. + + Parameters: + snmpV3Details (list of dict) - Cisco Catalyst Center details containing snmpV3 Credentials. + + Returns: + snmpV3 (list of dict) - Processed snmpV3 credential + data in the format suitable for the Cisco Catalyst Center config. + """ + + snmpV3 = [] + for item in snmpV3Details: + if item is None: + snmpV3.append(None) + else: + value = { + "username": item.get("username"), + "description": item.get("description"), + "snmpMode": item.get("snmpMode"), + "id": item.get("id"), + } + if value.get("snmpMode") == "AUTHNOPRIV": + value["authType"] = item.get("authType") + elif value.get("snmpMode") == "AUTHPRIV": + value.update({ + "authType": item.get("authType"), + "privacyType": item.get("privacyType") + }) + snmpV3.append(value) + return snmpV3 + + def get_cli_credentials(self, CredentialDetails, global_credentials): + """ + Get the current CLI Credential from + Cisco Catalyst Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + cliDetails (List) - The current CLI credentials. + """ + + # playbook CLI Credential details + all_CLI = CredentialDetails.get("cli_credential") + # All CLI details from Cisco Catalyst Center + cli_details = global_credentials.get("cliCredential") + # Cisco Catalyst Center details for the CLI Credential given in the playbook + cliDetails = [] + if all_CLI and cli_details: + for cliCredential in all_CLI: + cliDetail = None + cliId = cliCredential.get("id") + if cliId: + cliDetail = get_dict_result(cli_details, "id", cliId) + if not cliDetail: + self.msg = "CLI credential ID is invalid" + self.status = "failed" + return self + + cliOldDescription = cliCredential.get("old_description") + cliOldUsername = cliCredential.get("old_username") + if cliOldDescription and cliOldUsername and (not cliDetail): + for item in cli_details: + if item.get("description") == cliOldDescription \ + and item.get("username") == cliOldUsername: + if cliDetail: + self.msg = "More than one CLI credential with same \ + old_description and old_username. Pass ID." + self.status = "failed" + return self + cliDetail = item + if not cliDetail: + self.msg = "CLI credential old_description or old_username is invalid" + self.status = "failed" + return self + + cliDescription = cliCredential.get("description") + cliUsername = cliCredential.get("username") + if cliDescription and cliUsername and (not cliDetail): + for item in cli_details: + if item.get("description") == cliDescription \ + and item.get("username") == cliUsername: + if cliDetail: + self.msg = "More than one CLI Credential with same \ + description and username. Pass ID." + self.status = "failed" + return self + cliDetail = item + cliDetails.append(cliDetail) + return cliDetails + + def get_snmpV2cRead_credentials(self, CredentialDetails, global_credentials): + """ + Get the current snmpV2cRead Credential from + Cisco Catalyst Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + snmpV2cReadDetails (List) - The current snmpV2cRead. + """ + + # Playbook snmpV2cRead Credential details + all_snmpV2cRead = CredentialDetails.get("snmp_v2c_read") + # All snmpV2cRead details from the Cisco Catalyst Center + snmpV2cRead_details = global_credentials.get("snmpV2cRead") + # Cisco Catalyst Center details for the snmpV2cRead Credential given in the playbook + snmpV2cReadDetails = [] + if all_snmpV2cRead and snmpV2cRead_details: + for snmpV2cReadCredential in all_snmpV2cRead: + snmpV2cReadDetail = None + snmpV2cReadId = snmpV2cReadCredential.get("id") + if snmpV2cReadId: + snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) + if not snmpV2cReadDetail: + self.msg = "snmpV2cRead credential ID is invalid" + self.status = "failed" + return self + + snmpV2cReadOldDescription = snmpV2cReadCredential.get("old_description") + if snmpV2cReadOldDescription and (not snmpV2cReadDetail): + snmpV2cReadDetail = get_dict_result( + snmpV2cRead_details, + "description", + snmpV2cReadOldDescription + ) + if not snmpV2cReadDetail: + self.msg = "snmpV2cRead credential old_description is invalid" + self.status = "failed" + return self + + snmpV2cReadDescription = snmpV2cReadCredential.get("description") + if snmpV2cReadDescription and (not snmpV2cReadDetail): + snmpV2cReadDetail = get_dict_result( + snmpV2cRead_details, + "description", + snmpV2cReadDescription + ) + snmpV2cReadDetails.append(snmpV2cReadDetail) + return snmpV2cReadDetails + + def get_snmpV2cWrite_credentials(self, CredentialDetails, global_credentials): + """ + Get the current snmpV2cWrite Credential from + Cisco Catalyst Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + snmpV2cWriteDetails (List) - The current snmpV2cWrite. + """ + + # Playbook snmpV2cWrite Credential details + all_snmpV2cWrite = CredentialDetails.get("snmp_v2c_write") + # All snmpV2cWrite details from the Cisco Catalyst Center + snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") + # Cisco Catalyst Center details for the snmpV2cWrite Credential given in the playbook + snmpV2cWriteDetails = [] + if all_snmpV2cWrite and snmpV2cWrite_details: + for snmpV2cWriteCredential in all_snmpV2cWrite: + snmpV2cWriteDetail = None + snmpV2cWriteId = snmpV2cWriteCredential.get("id") + if snmpV2cWriteId: + snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) + if not snmpV2cWriteDetail: + self.msg = "snmpV2cWrite credential ID is invalid" + self.status = "failed" + return self + + snmpV2cWriteOldDescription = snmpV2cWriteCredential.get("old_description") + if snmpV2cWriteOldDescription and (not snmpV2cWriteDetail): + snmpV2cWriteDetail = get_dict_result( + snmpV2cWrite_details, + "description", + snmpV2cWriteOldDescription + ) + if not snmpV2cWriteDetail: + self.msg = "snmpV2cWrite credential old_description is invalid " + self.status = "failed" + return self + + snmpV2cWriteDescription = snmpV2cWriteCredential.get("description") + if snmpV2cWriteDescription and (not snmpV2cWriteDetail): + snmpV2cWriteDetail = get_dict_result( + snmpV2cWrite_details, + "description", + snmpV2cWriteDescription + ) + snmpV2cWriteDetails.append(snmpV2cWriteDetail) + return snmpV2cWriteDetails + + def get_httpsRead_credentials(self, CredentialDetails, global_credentials): + """ + Get the current httpsRead Credential from + Cisco Catalyst Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + httpsReadDetails (List) - The current httpsRead. + """ + + # Playbook httpsRead Credential details + all_httpsRead = CredentialDetails.get("https_read") + # All httpsRead details from the Cisco Catalyst Center + httpsRead_details = global_credentials.get("httpsRead") + # Cisco Catalyst Center details for the httpsRead Credential given in the playbook + httpsReadDetails = [] + if all_httpsRead and httpsRead_details: + for httpsReadCredential in all_httpsRead: + httpsReadDetail = None + httpsReadId = httpsReadCredential.get("id") + if httpsReadId: + httpsReadDetail = get_dict_result(httpsRead_details, "id", httpsReadId) + if not httpsReadDetail: + self.msg = "httpsRead credential Id is invalid" + self.status = "failed" + return self + + httpsReadOldDescription = httpsReadCredential.get("old_description") + httpsReadOldUsername = httpsReadCredential.get("old_username") + if httpsReadOldDescription and httpsReadOldUsername and (not httpsReadDetail): + for item in httpsRead_details: + if item.get("description") == httpsReadOldDescription \ + and item.get("username") == httpsReadOldUsername: + if httpsReadDetail: + self.msg = "More than one httpsRead credential with same \ + old_description and old_username. Pass ID." + self.status = "failed" + return self + httpsReadDetail = item + if not httpsReadDetail: + self.msg = "httpsRead credential old_description or old_username is invalid" + self.status = "failed" + return self + + httpsReadDescription = httpsReadCredential.get("description") + httpsReadUsername = httpsReadCredential.get("username") + if httpsReadDescription and httpsReadUsername and (not httpsReadDetail): + for item in httpsRead_details: + if item.get("description") == httpsReadDescription \ + and item.get("username") == httpsReadUsername: + if httpsReadDetail: + self.msg = "More than one httpsRead credential with same \ + description and username. Pass ID." + self.status = "failed" + return self + httpsReadDetail = item + httpsReadDetails.append(httpsReadDetail) + return httpsReadDetails + + def get_httpsWrite_credentials(self, CredentialDetails, global_credentials): + """ + Get the current httpsWrite Credential from + Cisco Catalyst Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + httpsWriteDetails (List) - The current httpsWrite. + """ + + # Playbook httpsWrite Credential details + all_httpsWrite = CredentialDetails.get("https_write") + # All httpsWrite details from the Cisco Catalyst Center + httpsWrite_details = global_credentials.get("httpsWrite") + # Cisco Catalyst Center details for the httpsWrite Credential given in the playbook + httpsWriteDetails = [] + if all_httpsWrite and httpsWrite_details: + for httpsWriteCredential in all_httpsWrite: + httpsWriteDetail = None + httpsWriteId = httpsWriteCredential.get("id") + if httpsWriteId: + httpsWriteDetail = get_dict_result(httpsWrite_details, "id", httpsWriteId) + if not httpsWriteDetail: + self.msg = "httpsWrite credential Id is invalid" + self.status = "failed" + return self + + httpsWriteOldDescription = httpsWriteCredential.get("old_description") + httpsWriteOldUsername = httpsWriteCredential.get("old_username") + if httpsWriteOldDescription and httpsWriteOldUsername and (not httpsWriteDetail): + for item in httpsWrite_details: + if item.get("description") == httpsWriteOldDescription \ + and item.get("username") == httpsWriteOldUsername: + if httpsWriteDetail: + self.msg = "More than one httpsWrite credential with same \ + old_description and old_username. Pass ID" + self.status = "failed" + return self + httpsWriteDetail = item + if not httpsWriteDetail: + self.msg = "httpsWrite credential old_description or \ + old_username is invalid" + self.status = "failed" + return self + + httpsWriteDescription = httpsWriteCredential.get("description") + httpsWriteUsername = httpsWriteCredential.get("username") + if httpsWriteDescription and httpsWriteUsername and (not httpsWriteDetail): + for item in httpsWrite_details: + if item.get("description") == httpsWriteDescription \ + and item.get("username") == httpsWriteUsername: + httpsWriteDetail = item + httpsWriteDetails.append(httpsWriteDetail) + return httpsWriteDetails + + def get_snmpV3_credentials(self, CredentialDetails, global_credentials): + """ + Get the current snmpV3 Credential from + Cisco Catalyst Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + snmpV3Details (List) - The current snmpV3. + """ + + # Playbook snmpV3 Credential details + all_snmpV3 = CredentialDetails.get("snmp_v3") + # All snmpV3 details from the Cisco Catalyst Center + snmpV3_details = global_credentials.get("snmpV3") + # Cisco Catalyst Center details for the snmpV3 Credential given in the playbook + snmpV3Details = [] + if all_snmpV3 and snmpV3_details: + for snmpV3Credential in all_snmpV3: + snmpV3Detail = None + snmpV3Id = snmpV3Credential.get("id") + if snmpV3Id: + snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) + if not snmpV3Detail: + self.msg = "snmpV3 credential id is invalid" + self.status = "failed" + return self + + snmpV3OldDescription = snmpV3Credential.get("old_description") + if snmpV3OldDescription and (not snmpV3Detail): + snmpV3Detail = get_dict_result(snmpV3_details, + "description", snmpV3OldDescription) + if not snmpV3Detail: + self.msg = "snmpV3 credential old_description is invalid" + self.status = "failed" + return self + + snmpV3Description = snmpV3Credential.get("description") + if snmpV3Description and (not snmpV3Detail): + snmpV3Detail = get_dict_result(snmpV3_details, "description", snmpV3Description) + snmpV3Details.append(snmpV3Detail) + return snmpV3Details + + def get_have_device_credentials(self, CredentialDetails): + """ + Get the current Global Device Credentials from + Cisco Catalyst Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + + Returns: + self - The current object with updated information. + """ + + global_credentials = self.get_global_credentials_params() + cliDetails = self.get_cli_credentials(CredentialDetails, global_credentials) + snmpV2cReadDetails = self.get_snmpV2cRead_credentials(CredentialDetails, global_credentials) + snmpV2cWriteDetails = self.get_snmpV2cWrite_credentials(CredentialDetails, + global_credentials) + httpsReadDetails = self.get_httpsRead_credentials(CredentialDetails, global_credentials) + httpsWriteDetails = self.get_httpsWrite_credentials(CredentialDetails, global_credentials) + snmpV3Details = self.get_snmpV3_credentials(CredentialDetails, global_credentials) + self.have.update({"globalCredential": {}}) + if cliDetails: + cliCredential = self.get_cli_params(cliDetails) + self.have.get("globalCredential").update({"cliCredential": cliCredential}) + if snmpV2cReadDetails: + snmpV2cRead = self.get_snmpV2cRead_params(snmpV2cReadDetails) + self.have.get("globalCredential").update({"snmpV2cRead": snmpV2cRead}) + if snmpV2cWriteDetails: + snmpV2cWrite = self.get_snmpV2cWrite_params(snmpV2cWriteDetails) + self.have.get("globalCredential").update({"snmpV2cWrite": snmpV2cWrite}) + if httpsReadDetails: + httpsRead = self.get_httpsRead_params(httpsReadDetails) + self.have.get("globalCredential").update({"httpsRead": httpsRead}) + if httpsWriteDetails: + httpsWrite = self.get_httpsWrite_params(httpsWriteDetails) + self.have.get("globalCredential").update({"httpsWrite": httpsWrite}) + if snmpV3Details: + snmpV3 = self.get_snmpV3_params(snmpV3Details) + self.have.get("globalCredential").update({"snmpV3": snmpV3}) + + self.log("Global device credential details: {0}" + .format(self.have.get("globalCredential")), "DEBUG") + self.msg = "Collected the Global Device Credential Details from the Cisco Catalyst Center" + self.status = "success" + return self + + def get_have(self, config): + """ + Get the current Global Device Credentials and + Device Credentials assigned to a site in Cisco Catalyst Center. + + Parameters: + config (dict) - Playbook details containing Global Device + Credentials configurations and Device Credentials should + be assigned to a site. + + Returns: + self - The current object with updated information of Global + Device Credentials and Device Credentials assigned to a site. + """ + + if config.get("global_credential_details") is not None: + CredentialDetails = config.get("global_credential_details") + self.get_have_device_credentials(CredentialDetails).check_return_status() + + self.log("Current State (have): {0}".format(self.have), "INFO") + self.msg = "Successfully retrieved the details from the Cisco Catalyst Center" + self.status = "success" + return self + + def get_want_device_credentials(self, CredentialDetails): + """ + Get the Global Device Credentials from the playbook. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + + Returns: + self - The current object with updated information of + Global Device Credentials from the playbook. + """ + + want = { + "want_create": {}, + "want_update": {} + } + if CredentialDetails.get("cli_credential"): + cli = CredentialDetails.get("cli_credential") + have_cli_ptr = 0 + create_cli_ptr = 0 + update_cli_ptr = 0 + values = ["password", "description", "username", "id"] + have_cliCredential = self.have.get("globalCredential").get("cliCredential") + for item in cli: + if not have_cliCredential or have_cliCredential[have_cli_ptr] is None: + if want.get("want_create").get("cliCredential") is None: + want.get("want_create").update({"cliCredential": []}) + create_credential = want.get("want_create").get("cliCredential") + create_credential.append({}) + for i in range(0, 3): + if item.get(values[i]): + create_credential[create_cli_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + cliCredential " + str(have_cli_ptr) + self.status = "failed" + return self + + if item.get("enable_password"): + create_credential[create_cli_ptr] \ + .update({"enablePassword": item.get("enable_password")}) + create_cli_ptr = create_cli_ptr + 1 + else: + if want.get("want_update").get("cliCredential") is None: + want.get("want_update").update({"cliCredential": []}) + update_credential = want.get("want_update").get("cliCredential") + update_credential.append({}) + if item.get("password"): + update_credential[update_cli_ptr] \ + .update({"password": item.get("password")}) + else: + self.msg = "password is mandatory for udpating \ + cliCredential " + str(have_cli_ptr) + self.status = "failed" + return self + + for i in range(1, 4): + if item.get(values[i]): + update_credential[update_cli_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_cli_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("cliCredential")[have_cli_ptr].get(values[i]) + }) + + if item.get("enable_password"): + update_credential[update_cli_ptr].update({ + "enablePassword": item.get("enable_password") + }) + update_cli_ptr = update_cli_ptr + 1 + have_cli_ptr = have_cli_ptr + 1 + + if CredentialDetails.get("snmp_v2c_read"): + snmpV2cRead = CredentialDetails.get("snmp_v2c_read") + have_snmpv2cread_ptr = 0 + create_snmpv2cread_ptr = 0 + update_snmpv2cread_ptr = 0 + values = ["read_community", "description", "id"] + keys = ["readCommunity", "description", "id"] + have_snmpV2cRead = self.have.get("globalCredential").get("snmpV2cRead") + for item in snmpV2cRead: + if not have_snmpV2cRead or have_snmpV2cRead[have_snmpv2cread_ptr] is None: + if want.get("want_create").get("snmpV2cRead") is None: + want.get("want_create").update({"snmpV2cRead": []}) + create_credential = want.get("want_create").get("snmpV2cRead") + create_credential.append({}) + for i in range(0, 2): + if item.get(values[i]): + create_credential[create_snmpv2cread_ptr] \ + .update({keys[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + snmpV2cRead " + str(have_snmpv2cread_ptr) + self.status = "failed" + return self + create_snmpv2cread_ptr = create_snmpv2cread_ptr + 1 + else: + if want.get("want_update").get("snmpV2cRead") is None: + want.get("want_update").update({"snmpV2cRead": []}) + update_credential = want.get("want_update").get("snmpV2cRead") + update_credential.append({}) + if item.get("read_community"): + update_credential[update_snmpv2cread_ptr] \ + .update({"readCommunity": item.get("read_community")}) + else: + self.msg = "read_community is mandatory for updating \ + snmpV2cRead " + str(have_snmpv2cread_ptr) + self.status = "failed" + return self + for i in range(1, 3): + if item.get(values[i]): + update_credential[update_snmpv2cread_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_snmpv2cread_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("snmpV2cRead")[have_snmpv2cread_ptr].get(values[i]) + }) + update_snmpv2cread_ptr = update_snmpv2cread_ptr + 1 + have_snmpv2cread_ptr = have_snmpv2cread_ptr + 1 + + if CredentialDetails.get("snmp_v2c_write"): + snmpV2cWrite = CredentialDetails.get("snmp_v2c_write") + have_snmpv2cwrite_ptr = 0 + create_snmpv2cwrite_ptr = 0 + update_snmpv2cwrite_ptr = 0 + values = ["write_community", "description", "id"] + keys = ["writeCommunity", "description", "id"] + have_snmpV2cWrite = self.have.get("globalCredential").get("snmpV2cWrite") + for item in snmpV2cWrite: + if not have_snmpV2cWrite or have_snmpV2cWrite[have_snmpv2cwrite_ptr] is None: + if want.get("want_create").get("snmpV2cWrite") is None: + want.get("want_create").update({"snmpV2cWrite": []}) + create_credential = want.get("want_create").get("snmpV2cWrite") + create_credential.append({}) + for i in range(0, 2): + if item.get(values[i]): + create_credential[create_snmpv2cwrite_ptr] \ + .update({keys[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + snmpV2cWrite " + str(have_snmpv2cwrite_ptr) + self.status = "failed" + return self + create_snmpv2cwrite_ptr = create_snmpv2cwrite_ptr + 1 + else: + if want.get("want_update").get("snmpV2cWrite") is None: + want.get("want_update").update({"snmpV2cWrite": []}) + update_credential = want.get("want_update").get("snmpV2cWrite") + update_credential.append({}) + if item.get("write_community"): + update_credential[update_snmpv2cwrite_ptr] \ + .update({"writeCommunity": item.get("write_community")}) + else: + self.msg = "write_community is mandatory for updating \ + snmpV2cWrite " + str(have_snmpv2cwrite_ptr) + self.status = "failed" + return self + for i in range(1, 3): + if item.get(values[i]): + update_credential[update_snmpv2cwrite_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_snmpv2cwrite_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("snmpV2cWrite")[have_snmpv2cwrite_ptr].get(values[i]) + }) + update_snmpv2cwrite_ptr = update_snmpv2cwrite_ptr + 1 + have_snmpv2cwrite_ptr = have_snmpv2cwrite_ptr + 1 + + if CredentialDetails.get("https_read"): + httpsRead = CredentialDetails.get("https_read") + have_httpsread_ptr = 0 + create_httpsread_ptr = 0 + update_httpsread_ptr = 0 + values = ["password", "description", "username", "id", "port"] + have_httpsRead = self.have.get("globalCredential").get("httpsRead") + for item in httpsRead: + self.log("Global credentials details: {0}" + .format(self.have.get("globalCredential")), "DEBUG") + if not have_httpsRead or have_httpsRead[have_httpsread_ptr] is None: + if want.get("want_create").get("httpsRead") is None: + want.get("want_create").update({"httpsRead": []}) + create_credential = want.get("want_create").get("httpsRead") + create_credential.append({}) + for i in range(0, 3): + if item.get(values[i]): + create_credential[create_httpsread_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + httpsRead " + str(have_httpsread_ptr) + self.status = "failed" + return self + if item.get("port"): + create_credential[create_httpsread_ptr] \ + .update({"port": item.get("port")}) + else: + create_credential[create_httpsread_ptr] \ + .update({"port": "443"}) + create_httpsread_ptr = create_httpsread_ptr + 1 + else: + if want.get("want_update").get("httpsRead") is None: + want.get("want_update").update({"httpsRead": []}) + update_credential = want.get("want_update").get("httpsRead") + update_credential.append({}) + if item.get("password"): + update_credential[update_httpsread_ptr] \ + .update({"password": item.get("password")}) + else: + self.msg = "password is mandatory for updating \ + httpsRead " + str(have_httpsread_ptr) + self.status = "failed" + return self + for i in range(1, 5): + if item.get(values[i]): + update_credential[update_httpsread_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_httpsread_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("httpsRead")[have_httpsread_ptr].get(values[i]) + }) + update_httpsread_ptr = update_httpsread_ptr + 1 + have_httpsread_ptr = have_httpsread_ptr + 1 + + if CredentialDetails.get("https_write"): + httpsWrite = CredentialDetails.get("https_write") + have_httpswrite_ptr = 0 + create_httpswrite_ptr = 0 + update_httpswrite_ptr = 0 + values = ["password", "description", "username", "id", "port"] + have_httpsWrite = self.have.get("globalCredential").get("httpsWrite") + for item in httpsWrite: + if not have_httpsWrite or have_httpsWrite[have_httpswrite_ptr] is None: + if want.get("want_create").get("httpsWrite") is None: + want.get("want_create").update({"httpsWrite": []}) + create_credential = want.get("want_create").get("httpsWrite") + create_credential.append({}) + for i in range(0, 3): + if item.get(values[i]): + create_credential[create_httpswrite_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + httpsWrite " + str(have_httpswrite_ptr) + self.status = "failed" + return self + if item.get("port"): + create_credential[create_httpswrite_ptr] \ + .update({"port": item.get("port")}) + else: + create_credential[create_httpswrite_ptr] \ + .update({"port": "443"}) + create_httpswrite_ptr = create_httpswrite_ptr + 1 + else: + if want.get("want_update").get("httpsWrite") is None: + want.get("want_update").update({"httpsWrite": []}) + update_credential = want.get("want_update").get("httpsWrite") + update_credential.append({}) + if item.get("password"): + update_credential[update_httpswrite_ptr] \ + .update({"password": item.get("password")}) + else: + self.msg = "password is mandatory for updating \ + httpsRead " + str(have_httpswrite_ptr) + self.status = "failed" + return self + for i in range(1, 5): + if item.get(values[i]): + update_credential[update_httpswrite_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_httpswrite_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("httpsWrite")[have_httpswrite_ptr].get(values[i]) + }) + update_httpswrite_ptr = update_httpswrite_ptr + 1 + have_httpswrite_ptr = have_httpswrite_ptr + 1 + + if CredentialDetails.get("snmp_v3"): + snmpV3 = CredentialDetails.get("snmp_v3") + have_snmpv3_ptr = 0 + create_snmpv3_ptr = 0 + update_snmpv3_ptr = 0 + values = ["description", "username", "id"] + have_snmpV3 = self.have.get("globalCredential").get("snmpV3") + for item in snmpV3: + if not have_snmpV3 or have_snmpV3[have_snmpv3_ptr] is None: + if want.get("want_create").get("snmpV3") is None: + want.get("want_create").update({"snmpV3": []}) + create_credential = want.get("want_create").get("snmpV3") + create_credential.append({}) + for i in range(0, 2): + if item.get(values[i]): + create_credential[create_snmpv3_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + snmpV3 " + str(have_snmpv3_ptr) + self.status = "failed" + return self + if item.get("snmp_mode"): + create_credential[create_snmpv3_ptr] \ + .update({"snmpMode": item.get("snmp_mode")}) + else: + create_credential[create_snmpv3_ptr] \ + .update({"snmpMode": "AUTHPRIV"}) + if create_credential[create_snmpv3_ptr].get("snmpMode") == "AUTHNOPRIV" or \ + create_credential[create_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": + auths = ["auth_password", "auth_type"] + keys = { + "auth_password": "authPassword", + "auth_type": "authType" + } + for auth in auths: + if item.get(auth): + create_credential[create_snmpv3_ptr] \ + .update({keys[auth]: item.get(auth)}) + else: + self.msg = auth + " is mandatory for creating \ + snmpV3 " + str(have_snmpv3_ptr) + self.status = "failed" + return self + if len(item.get("auth_password")) < 8: + self.msg = "auth_password length should be greater than 8" + self.status = "failed" + return self + self.log("snmp_mode: {0}".format(create_credential[create_snmpv3_ptr] + .get("snmpMode")), "DEBUG") + if create_credential[create_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": + privs = ["privacy_password", "privacy_type"] + key = { + "privacy_password": "privacyPassword", + "privacy_type": "privacyType" + } + for priv in privs: + if item.get(priv): + create_credential[create_snmpv3_ptr] \ + .update({key[priv]: item.get(priv)}) + else: + self.msg = priv + " is mandatory for creating \ + snmpV3 " + str(have_snmpv3_ptr) + self.status = "failed" + return self + if len(item.get("privacy_password")) < 8: + self.msg = "privacy_password should be greater than 8" + self.status = "failed" + return self + elif create_credential[create_snmpv3_ptr].get("snmpMode") != "NOAUTHNOPRIV": + self.msg = "snmp_mode in snmpV3 is not \ + ['AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV']" + self.status = "failed" + return self + create_snmpv3_ptr = create_snmpv3_ptr + 1 + else: + if want.get("want_update").get("snmpV3") is None: + want.get("want_update").update({"snmpV3": []}) + update_credential = want.get("want_update").get("snmpV3") + update_credential.append({}) + for value in values: + if item.get(value): + update_credential[update_snmpv3_ptr] \ + .update({value: item.get(value)}) + else: + update_credential[update_snmpv3_ptr].update({ + value: self.have.get("globalCredential") + .get("snmpV3")[have_snmpv3_ptr].get(value) + }) + if item.get("snmp_mode"): + update_credential[update_snmpv3_ptr] \ + .update({"snmpMode": item.get("snmp_mode")}) + if update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHNOPRIV" or \ + update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": + if item.get("auth_type"): + update_credential[update_snmpv3_ptr] \ + .update({"authType": item.get("auth_type")}) + elif self.have.get("globalCredential") \ + .get("snmpMode")[have_snmpv3_ptr].get("authType"): + update_credential[update_snmpv3_ptr].update({ + "authType": self.have.get("globalCredential") + .get("snmpMode")[have_snmpv3_ptr].get("authType") + }) + else: + self.msg = "auth_type is required for updating snmpV3 " + \ + str(have_snmpv3_ptr) + self.status = "failed" + return self + if item.get("auth_password"): + update_credential[update_snmpv3_ptr] \ + .update({"authPassword": item.get("auth_password")}) + else: + self.msg = "auth_password is required for updating snmpV3 " + \ + str(have_snmpv3_ptr) + self.status = "failed" + return self + if len(item.get("auth_password")) < 8: + self.msg = "auth_password length should be greater than 8" + self.status = "failed" + return self + elif update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": + if item.get("privacy_type"): + update_credential[update_snmpv3_ptr] \ + .update({"privacyType": item.get("privacy_type")}) + elif self.have.get("globalCredential") \ + .get("snmpMode")[have_snmpv3_ptr].get("privacyType"): + update_credential[update_snmpv3_ptr].update({ + "privacyType": self.have.get("globalCredential") + .get("snmpMode")[have_snmpv3_ptr].get("privacyType") + }) + else: + self.msg = "privacy_type is required for updating snmpV3 " + \ + str(have_snmpv3_ptr) + self.status = "failed" + return self + if item.get("privacy_password"): + update_credential[update_snmpv3_ptr] \ + .update({"privacyPassword": item.get("privacy_password")}) + else: + self.msg = "privacy_password is required for updating snmpV3 " + \ + str(have_snmpv3_ptr) + self.status = "failed" + return self + if len(item.get("privacy_password")) < 8: + self.msg = "privacy_password length should be greater than 8" + self.status = "failed" + return self + update_snmpv3_ptr = update_snmpv3_ptr + 1 + have_snmpv3_ptr = have_snmpv3_ptr + 1 + self.want.update(want) + self.msg = "Collected the Global Credentials from the Cisco Catalyst Center" + self.status = "success" + return self + + def get_want_assign_credentials(self, AssignCredentials): + """ + Get the Credentials to be assigned to a site from the playbook. + Check this API using the check_return_status. + + Parameters: + AssignCredentials (dict) - Playbook details containing + credentials that need to be assigned to a site. + + Returns: + self - The current object with updated information of credentials + that need to be assigned to a site from the playbook. + """ + want = { + "assign_credentials": {} + } + site_name = AssignCredentials.get("site_name") + if not site_name: + self.msg = "site_name is required for AssignCredentials" + self.status = "failed" + return self + site_id = [] + for site_name in site_name: + siteId = self.get_site_id(site_name) + if not site_name: + self.msg = "site_name is invalid in AssignCredentials" + self.status = "failed" + return self + site_id.append(siteId) + want.update({"site_id": site_id}) + global_credentials = self.get_global_credentials_params() + cli_credential = AssignCredentials.get("cli_credential") + if cli_credential: + cliId = cli_credential.get("id") + cliDescription = cli_credential.get("description") + cliUsername = cli_credential.get("username") + + if cliId or cliDescription and cliUsername: + # All CLI details from the Cisco Catalyst Center + cli_details = global_credentials.get("cliCredential") + if not cli_details: + self.msg = "Global CLI credential is not available" + self.status = "failed" + return self + cliDetail = None + if cliId: + cliDetail = get_dict_result(cli_details, "id", cliId) + if not cliDetail: + self.msg = "The ID for the CLI credential is not valid." + self.status = "failed" + return self + elif cliDescription and cliUsername: + for item in cli_details: + if item.get("description") == cliDescription and \ + item.get("username") == cliUsername: + cliDetail = item + if not cliDetail: + self.msg = "The username and description of the CLI credential are invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"cliId": cliDetail.get("id")}) + + snmp_v2c_read = AssignCredentials.get("snmp_v2c_read") + if snmp_v2c_read: + snmpV2cReadId = snmp_v2c_read.get("id") + snmpV2cReadDescription = snmp_v2c_read.get("description") + if snmpV2cReadId or snmpV2cReadDescription: + + # All snmpV2cRead details from the Cisco Catalyst Center + snmpV2cRead_details = global_credentials.get("snmpV2cRead") + if not snmpV2cRead_details: + self.msg = "Global snmpV2cRead credential is not available" + self.status = "failed" + return self + snmpV2cReadDetail = None + if snmpV2cReadId: + snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) + if not snmpV2cReadDetail: + self.msg = "The ID of the snmpV2cRead credential is not valid." + self.status = "failed" + return self + elif snmpV2cReadDescription: + for item in snmpV2cRead_details: + if item.get("description") == snmpV2cReadDescription: + snmpV2cReadDetail = item + if not snmpV2cReadDetail: + self.msg = "The username and description for the snmpV2cRead credential are invalid." + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV2ReadId": snmpV2cReadDetail.get("id")}) + + snmp_v2c_write = AssignCredentials.get("snmp_v2c_write") + if snmp_v2c_write: + snmpV2cWriteId = snmp_v2c_write.get("id") + snmpV2cWriteDescription = snmp_v2c_write.get("description") + if snmpV2cWriteId or snmpV2cWriteDescription: + + # All snmpV2cWrite details from the Cisco Catalyst Center + snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") + if not snmpV2cWrite_details: + self.msg = "Global snmpV2cWrite Credential is not available" + self.status = "failed" + return self + snmpV2cWriteDetail = None + if snmpV2cWriteId: + snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) + if not snmpV2cWriteDetail: + self.msg = "The ID of the snmpV2cWrite credential is invalid." + self.status = "failed" + return self + elif snmpV2cWriteDescription: + for item in snmpV2cWrite_details: + if item.get("description") == snmpV2cWriteDescription: + snmpV2cWriteDetail = item + if not snmpV2cWriteDetail: + self.msg = "The username and description of the snmpV2cWrite credential are invalid." + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV2WriteId": snmpV2cWriteDetail.get("id")}) + + https_read = AssignCredentials.get("https_read") + if https_read: + httpReadId = https_read.get("id") + httpReadDescription = https_read.get("description") + httpReadUsername = https_read.get("username") + if httpReadId or httpReadDescription and httpReadUsername: + + # All httpRead details from the Cisco Catalyst Center + httpRead_details = global_credentials.get("httpsRead") + if not httpRead_details: + self.msg = "Global httpRead Credential is not available." + self.status = "failed" + return self + httpReadDetail = None + if httpReadId: + httpReadDetail = get_dict_result(httpRead_details, "id", httpReadId) + if not httpReadDetail: + self.msg = "The ID of the httpRead credential is not valid." + self.status = "failed" + return self + elif httpReadDescription and httpReadUsername: + for item in httpRead_details: + if item.get("description") == httpReadDescription and \ + item.get("username") == httpReadUsername: + httpReadDetail = item + if not httpReadDetail: + self.msg = "The description and username for the httpRead credential are invalid." + self.status = "failed" + return self + want.get("assign_credentials").update({"httpRead": httpReadDetail.get("id")}) + + https_write = AssignCredentials.get("https_write") + if https_write: + httpWriteId = https_write.get("id") + httpWriteDescription = https_write.get("description") + httpWriteUsername = https_write.get("username") + if httpWriteId or httpWriteDescription and httpWriteUsername: + + # All httpWrite details from the Cisco Catalyst Center + httpWrite_details = global_credentials.get("httpsWrite") + if not httpWrite_details: + self.msg = "Global httpWrite credential is not available." + self.status = "failed" + return self + httpWriteDetail = None + if httpWriteId: + httpWriteDetail = get_dict_result(httpWrite_details, "id", httpWriteId) + if not httpWriteDetail: + self.msg = "The ID of the httpWrite credential is not valid." + self.status = "failed" + return self + elif httpWriteDescription and httpWriteUsername: + for item in httpWrite_details: + if item.get("description") == httpWriteDescription and \ + item.get("username") == httpWriteUsername: + httpWriteDetail = item + if not httpWriteDetail: + self.msg = "The description and username for the httpWrite credential are invalid." + self.status = "failed" + return self + want.get("assign_credentials").update({"httpWrite": httpWriteDetail.get("id")}) + + snmp_v3 = AssignCredentials.get("snmp_v3") + if snmp_v3: + snmpV3Id = snmp_v3.get("id") + snmpV3Description = snmp_v3.get("description") + if snmpV3Id or snmpV3Description: + + # All snmpV3 details from the Cisco Catalyst Center + snmpV3_details = global_credentials.get("snmpV3") + if not snmpV3_details: + self.msg = "Global snmpV3 Credential is not available." + self.status = "failed" + return self + snmpV3Detail = None + if snmpV3Id: + snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) + if not snmpV3Detail: + self.msg = "The ID of the snmpV3 credential is not valid." + self.status = "failed" + return self + elif snmpV3Description: + for item in snmpV3_details: + if item.get("description") == snmpV3Description: + snmpV3Detail = item + if not snmpV3Detail: + self.msg = "The username and description for the snmpV2cWrite credential are invalid." + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV3Id": snmpV3Detail.get("id")}) + self.log("Desired State (want): {0}".format(want), "INFO") + self.want.update(want) + self.msg = "Collected the Credentials needed to be assigned from the Cisco Catalyst Center" + self.status = "success" + return self + + def get_want(self, config): + """ + Get the current Global Device Credentials and Device + Credentials assigned to a site form the playbook. + + Parameters: + config (dict) - Playbook details containing Global Device + Credentials configurations and Device Credentials should + be assigned to a site. + + Returns: + self - The current object with updated information of Global + Device Credentials and Device Credentials assigned to a site. + """ + + if config.get("global_credential_details"): + CredentialDetails = config.get("global_credential_details") + self.get_want_device_credentials(CredentialDetails).check_return_status() + + if config.get("assign_credentials_to_site"): + AssignCredentials = config.get("assign_credentials_to_site") + self.get_want_assign_credentials(AssignCredentials).check_return_status() + + self.log("Desired State (want): {0}".format(self.want), "INFO") + self.msg = "Successfully retrieved details from the playbook" + self.status = "success" + return self + + def create_device_credentials(self): + """ + Create Global Device Credential to the Cisco Catalyst + Center based on the provided playbook details. + Check the return value of the API with check_return_status(). + + Parameters: + self + + Returns: + self + """ + + result_global_credential = self.result.get("response")[0].get("globalCredential") + want_create = self.want.get("want_create") + if not want_create: + result_global_credential.update({ + "No Creation": { + "response": "No Response", + "msg": "No Creation is available" + } + }) + return self + + credential_params = want_create + self.log("Creating global credential API input parameters: {0}" + .format(credential_params), "DEBUG") + response = self.dnac._exec( + family="discovery", + function='create_global_credentials_v2', + params=credential_params, + ) + self.log("Received API response from 'create_global_credentials_v2': {0}" + .format(response), "DEBUG") + validation_string = "global credential addition performed" + self.check_task_response_status(response, validation_string).check_return_status() + self.log("Global credential created successfully", "INFO") + result_global_credential.update({ + "Creation": { + "response": credential_params, + "msg": "Global Credential Created Successfully" + } + }) + self.msg = "Global Device Credential Created Successfully" + self.status = "success" + return self + + def update_device_credentials(self): + """ + Update Device Credential to the Cisco Catalyst Center based on the provided playbook details. + Check the return value of the API with check_return_status(). + + Parameters: + self + + Returns: + self + """ + + result_global_credential = self.result.get("response")[0].get("globalCredential") + + # Get the result global credential and want_update from the current object + want_update = self.want.get("want_update") + # If no credentials to update, update the result and return + if not want_update: + result_global_credential.update({ + "No Updation": { + "response": "No Response", + "msg": "No Updation is available" + } + }) + self.msg = "No Updation is available" + self.status = "success" + return self + i = 0 + flag = True + values = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", + "httpsRead", "httpsWrite", "snmpV3"] + final_response = [] + self.log("Desired State for global device credentials updation: {0}" + .format(want_update), "DEBUG") + while flag: + flag = False + credential_params = {} + for value in values: + if want_update.get(value) and i < len(want_update.get(value)): + flag = True + credential_params.update({value: want_update.get(value)[i]}) + i = i + 1 + if credential_params: + final_response.append(credential_params) + response = self.dnac._exec( + family="discovery", + function='update_global_credentials_v2', + params=credential_params, + ) + self.log("Received API response for 'update_global_credentials_v2': {0}" + .format(response), "DEBUG") + validation_string = "global credential update performed" + self.check_task_response_status(response, validation_string).check_return_status() + self.log("Updating device credential API input parameters: {0}" + .format(final_response), "DEBUG") + self.log("Global device credential updated successfully", "INFO") + result_global_credential.update({ + "Updation": { + "response": final_response, + "msg": "Global Device Credential Updated Successfully" + } + }) + self.msg = "Global Device Credential Updated Successfully" + self.status = "success" + return self + + def assign_credentials_to_site(self): + """ + Assign Global Device Credential to the Cisco Catalyst + Center based on the provided playbook details. + Check the return value of the API with check_return_status(). + + Parameters: + self + + Returns: + self + """ + + result_assign_credential = self.result.get("response")[0].get("assignCredential") + credential_params = self.want.get("assign_credentials") + final_response = [] + self.log("Assigning device credential to site API input parameters: {0}" + .format(credential_params), "DEBUG") + if not credential_params: + result_assign_credential.update({ + "No Assign Credentials": { + "response": "No Response", + "msg": "No Assignment is available" + } + }) + self.msg = "No Assignment is available" + self.status = "success" + return self + + site_ids = self.want.get("site_id") + for site_id in site_ids: + credential_params.update({"site_id": site_id}) + final_response.append(copy.deepcopy(credential_params)) + response = self.dnac._exec( + family="network_settings", + function='assign_device_credential_to_site_v2', + params=credential_params, + ) + self.log("Received API response for 'assign_device_credential_to_site_v2': {0}" + .format(response), "DEBUG") + validation_string = "desired common settings operation successful" + self.check_task_response_status(response, validation_string).check_return_status() + self.log("Device credential assigned to site {0} is successfully." + .format(site_ids), "INFO") + self.log("Desired State for assign credentials to a site: {0}" + .format(final_response), "DEBUG") + result_assign_credential.update({ + "Assign Credentials": { + "response": final_response, + "msg": "Device Credential Assigned to a site is Successfully" + } + }) + self.msg = "Global Credential is assigned Successfully" + self.status = "success" + return self + + def get_diff_merged(self, config): + """ + Update or Create Global Device Credential and assign device + credential to a site in Cisco Catalyst Center based on the playbook provided. + + Parameters: + config (list of dict) - Playbook details containing Global + Device Credential and assign credentials to a site information. + + Returns: + self + """ + + if config.get("global_credential_details") is not None: + self.create_device_credentials().check_return_status() + + if config.get("global_credential_details") is not None: + self.update_device_credentials().check_return_status() + + if config.get("assign_credentials_to_site") is not None: + self.assign_credentials_to_site().check_return_status() + + return self + + def delete_device_credential(self, config): + """ + Delete Global Device Credential in Cisco Catalyst Center based on the playbook details. + Check the return value of the API with check_return_status(). + + Parameters: + config (dict) - Playbook details containing Global Device Credential information. + self - The current object details. + + Returns: + self + """ + + result_global_credential = self.result.get("response")[0].get("globalCredential") + have_values = self.have.get("globalCredential") + final_response = {} + self.log("Global device credentials to be deleted: {0}".format(have_values), "DEBUG") + credential_mapping = { + "cliCredential": "cli_credential", + "snmpV2cRead": "snmp_v2c_read", + "snmpV2cWrite": "snmp_v2c_write", + "snmpV3": "snmp_v3", + "httpsRead": "https_read", + "httpsWrite": "https_write" + } + for item in have_values: + config_itr = 0 + final_response.update({item: []}) + for value in have_values.get(item): + if value is None: + self.log("Credential Name: {0}".format(item), "DEBUG") + self.log("Credential Item: {0}".format(config.get("global_credential_details") + .get(credential_mapping.get(item))), "DEBUG") + final_response.get(item).append( + str(config.get("global_credential_details") + .get(credential_mapping.get(item))[config_itr]) + " is not found." + ) + continue + _id = have_values.get(item)[config_itr].get("id") + response = self.dnac._exec( + family="discovery", + function="delete_global_credential_v2", + params={"id": _id}, + ) + self.log("Received API response for 'delete_global_credential_v2': {0}" + .format(response), "DEBUG") + validation_string = "global credential deleted successfully" + self.check_task_response_status(response, validation_string).check_return_status() + final_response.get(item).append(_id) + config_itr = config_itr + 1 + + self.log("Deleting device credential API input parameters: {0}" + .format(final_response), "DEBUG") + self.log("Successfully deleted global device credential.", "INFO") + result_global_credential.update({ + "Deletion": { + "response": final_response, + "msg": "Global Device Credentials Deleted Successfully" + } + }) + self.msg = "Global Device Credentials Updated Successfully" + self.status = "success" + return self + + def get_diff_deleted(self, config): + """ + Delete Global Device Credential in Cisco Catalyst Center based on the playbook details. + + Parameters: + config (dict) - Playbook details containing Global Device Credential information. + self - The current object details. + + Returns: + self + """ + + if config.get("global_credential_details") is not None: + self.delete_device_credential(config).check_return_status() + + return self + + def verify_diff_merged(self, config): + """ + Validating the Cisco Catalyst Center configuration with the playbook details + when state is merged (Create/Update). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + self.log(str("Entered the verify function."), "DEBUG") + self.get_have(config) + self.get_want(config) + self.log("Current State (have): {0}".format(self.have), "INFO") + self.log("Desired State (want): {0}".format(self.want), "INFO") + + if config.get("global_credential_details") is not None: + if self.want.get("want_create"): + self.msg = "Global Device Credentials config is not applied to the Cisco Catalyst Center" + self.status = "failed" + return self + + if self.want.get("want_update"): + credential_types = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", + "httpsRead", "httpsWrite", "snmpV3"] + value_mapping = { + "cliCredential": ["username", "description", "id"], + "snmpV2cRead": ["description", "id"], + "snmpV2cWrite": ["description", "id"], + "httpsRead": ["description", "username", "port", "id"], + "httpsWrite": ["description", "username", "port", "id"], + "snmpV3": ["username", "description", "snmpMode", "id"] + } + for credential_type in credential_types: + if self.want.get(credential_type): + want_credential = self.want.get(credential_type) + if self.have.get(credential_type): + have_credential = self.have.get(credential_type) + values = value_mapping.get(credential_type) + for value in values: + equality = have_credential.get(value) is want_credential.get(value) + if not have_credential or not equality: + self.msg = "{0} config is not applied ot the Cisco Catalyst Center".format(credential_type) + self.status = "failed" + return self + + self.log("Successfully validated global device credential", "INFO") + self.result.get("response")[0].get("globalCredential").update({"Validation": "Success"}) + + if config.get("assign_credentials_to_site") is not None: + self.log("Successfully validated the assign device credential to site", "INFO") + self.result.get("response")[0].get("assignCredential").update({"Validation": "Success"}) + + self.msg = "Successfully validated the Global Device Credential and \ + Assign Device Credential to Site." + self.status = "success" + return self + + def verify_diff_deleted(self, config): + """ + Validating the Cisco Catalyst Center configuration with the playbook details + when state is deleted (delete). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + self.get_have(config) + self.log("Current State (have): {0}".format(self.have), "INFO") + self.log("Desired State (want): {0}".format(self.want), "INFO") + + if config.get("global_credential_details") is not None: + have_global_credential = self.have.get("globalCredential") + credential_types = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", + "httpsRead", "httpsWrite", "snmpV3"] + for credential_type in credential_types: + for item in have_global_credential.get(credential_type): + if item is not None: + self.msg = "Delete Global Device Credentials config \ + is not applied to the config" + self.status = "failed" + return self + + self.log("Successfully validated absence of global device credential.", "INFO") + self.result.get("response")[0].get("globalCredential").update({"Validation": "Success"}) + + self.msg = "Successfully validated the absence of Global Device Credential." + self.status = "success" + return self + + def reset_values(self): + """ + Reset all neccessary attributes to default values + + Parameters: + self + + Returns: + self + """ + + self.have.clear() + self.want.clear() + return self + + +def main(): + """main entry point for module execution""" + + # Define the specification for module arguments + element_spec = { + "dnac_host": {"type": 'str', "required": True}, + "dnac_port": {"type": 'str', "default": '443'}, + "dnac_username": {"type": 'str', "default": 'admin', "aliases": ['user']}, + "dnac_password": {"type": 'str', "no_log": True}, + "dnac_verify": {"type": 'bool', "default": 'True'}, + "dnac_version": {"type": 'str', "default": '2.2.3.3'}, + "dnac_debug": {"type": 'bool', "default": False}, + "dnac_log": {"type": 'bool', "default": False}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + "config_verify": {"type": 'bool', "default": False}, + "config": {"type": 'list', "required": True, "elements": 'dict'}, + "state": {"default": 'merged', "choices": ['merged', 'deleted']}, + "validate_response_schema": {"type": 'bool', "default": True}, + } + + # Create an AnsibleModule object with argument specifications + module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) + ccc_credential = DeviceCredential(module) + state = ccc_credential.params.get("state") + config_verify = ccc_credential.params.get("config_verify") + if state not in ccc_credential.supported_states: + ccc_credential.status = "invalid" + ccc_credential.msg = "State {0} is invalid".format(state) + ccc_credential.check_return_status() + + ccc_credential.validate_input().check_return_status() + + for config in ccc_credential.config: + ccc_credential.reset_values() + ccc_credential.get_have(config).check_return_status() + if state != "deleted": + ccc_credential.get_want(config).check_return_status() + ccc_credential.get_diff_state_apply[state](config).check_return_status() + if config_verify: + ccc_credential.verify_diff_state_apply[state](config).check_return_status() + + module.exit_json(**ccc_credential.result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py new file mode 100644 index 0000000000..1c0539ab52 --- /dev/null +++ b/plugins/modules/network_settings_workflow_manager.py @@ -0,0 +1,2160 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2023, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Ansible module to perform operations on global pool, reserve pool and network in Cisco Catalyst Center.""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ['Muthu Rakesh, Madhan Sankaranarayanan'] + +DOCUMENTATION = r""" +--- +module: network_settings_workflow_manager +short_description: Resource module for IP Address pools and network functions +description: +- Manage operations on Global Pool, Reserve Pool, Network resources. +- API to create/update/delete global pool. +- API to reserve/update/delete an ip subpool from the global pool. +- API to update network settings for DHCP, Syslog, SNMP, NTP, Network AAA, Client and Endpoint AAA, + and/or DNS center server settings. +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Muthu Rakesh (@MUTHU-RAKESH-27) + Madhan Sankaranarayanan (@madhansansel) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center after applying the playbook config. + type: bool + default: False + state: + description: The state of Cisco Catalyst Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of global pool, reserved pool, network being managed. + type: list + elements: dict + required: true + suboptions: + global_pool_details: + description: Global ip pool manages IPv4 and IPv6 IP pools. + type: dict + suboptions: + settings: + description: Global Pool's settings. + type: dict + suboptions: + ip_pool: + description: Global Pool's ippool. + elements: dict + type: list + suboptions: + dhcp_server_ips: + description: Dhcp Server Ips. + elements: str + type: list + dns_server_ips: + description: Dns Server Ips. + elements: str + type: list + gateway: + description: Gateway. + type: str + ip_address_space: + description: Ip address space. + type: str + cidr: + description: Ip pool cidr. + type: str + prev_name: + description: previous name. + type: str + name: + description: Ip Pool Name. + type: str + reserve_pool_details: + description: Reserving IP subpool from the global pool + type: dict + suboptions: + ipv4DhcpServers: + description: IPv4 input for dhcp server ip example 1.1.1.1. + elements: str + type: list + ipv4_dns_servers: + description: IPv4 input for dns server ip example 4.4.4.4. + elements: str + type: list + ipv4GateWay: + description: Gateway ip address details, example 175.175.0.1. + type: str + version_added: 4.0.0 + ipv4_global_pool: + description: IP v4 Global pool address with cidr, example 175.175.0.0/16. + type: str + ipv4_prefix: + description: ip4 prefix length is enabled or ipv4 total Host input is enabled + type: bool + ipv4_prefix_length: + description: The ipv4 prefix length is required when ipv4_prefix value is true. + type: int + ipv4_subnet: + description: IPv4 Subnet address, example 175.175.0.0. + type: str + ipv4TotalHost: + description: IPv4 total host is required when ipv4_prefix value is false. + type: int + ipv6_address_space: + description: > + If the value is false only ipv4 input are required, otherwise both + ipv6 and ipv4 are required. + type: bool + ipv6DhcpServers: + description: IPv6 format dhcp server as input example 2001 db8 1234. + elements: str + type: list + ipv6DnsServers: + description: IPv6 format dns server input example 2001 db8 1234. + elements: str + type: list + ipv6GateWay: + description: Gateway ip address details, example 2001 db8 85a3 0 100 1. + type: str + ipv6_global_pool: + description: > + IPv6 Global pool address with cidr this is required when ipv6_address_space + value is true, example 2001 db8 85a3 /64. + type: str + ipv6_prefix: + description: > + Ipv6 prefix value is true, the ip6 prefix length input field is enabled, + if it is false ipv6 total Host input is enable. + type: bool + ipv6_prefix_length: + description: IPv6 prefix length is required when the ipv6_prefix value is true. + type: int + ipv6_subnet: + description: IPv6 Subnet address, example 2001 db8 85a3 0 100. + type: str + ipv6TotalHost: + description: IPv6 total host is required when ipv6_prefix value is false. + type: int + name: + description: Name of the reserve ip sub pool. + type: str + prev_name: + description: Previous name of the reserve ip sub pool. + type: str + site_name: + description: Site name path parameter. Site name to reserve the ip sub pool. + type: str + slaac_support: + description: Slaac Support. + type: bool + type: + description: Type of the reserve ip sub pool. + type: str + network_management_details: + description: Set default network settings for the site + type: dict + suboptions: + settings: + description: Network management details settings. + type: dict + suboptions: + client_and_endpoint_aaa: + description: Network V2's clientAndEndpoint_aaa. + suboptions: + ip_address: + description: IP address for ISE serve (eg 1.1.1.4). + type: str + network: + description: IP address for AAA or ISE server (eg 2.2.2.1). + type: str + protocol: + description: Protocol for AAA or ISE serve (eg RADIUS). + type: str + servers: + description: Server type AAA or ISE server (eg AAA). + type: str + shared_secret: + description: Shared secret for ISE server. + type: str + type: dict + dhcp_server: + description: DHCP Server IP (eg 1.1.1.1). + elements: str + type: list + dns_server: + description: Network V2's dnsServer. + suboptions: + domain_name: + description: Domain Name of DHCP (eg; cisco). + type: str + primary_ip_address: + description: Primary IP Address for DHCP (eg 2.2.2.2). + type: str + secondary_ip_address: + description: Secondary IP Address for DHCP (eg 3.3.3.3). + type: str + type: dict + message_of_the_day: + description: Network V2's messageOfTheday. + suboptions: + banner_message: + description: Massage for Banner message (eg; Good day). + type: str + retain_existing_banner: + description: Retain existing Banner Message (eg "true" or "false"). + type: str + type: dict + netflow_collector: + description: Network V2's netflowcollector. + suboptions: + ip_address: + description: IP Address for NetFlow collector (eg 3.3.3.1). + type: str + port: + description: Port for NetFlow Collector (eg; 443). + type: int + type: dict + network_aaa: + description: Network V2's network_aaa. + suboptions: + ip_address: + description: IP address for AAA and ISE server (eg 1.1.1.1). + type: str + network: + description: IP Address for AAA or ISE server (eg 2.2.2.2). + type: str + protocol: + description: Protocol for AAA or ISE serve (eg RADIUS). + type: str + servers: + description: Server type for AAA Network (eg AAA). + type: str + shared_secret: + description: Shared secret for ISE Server. + type: str + type: dict + ntp_server: + description: IP address for NTP server (eg 1.1.1.2). + elements: str + type: list + snmp_server: + description: Network V2's snmpServer. + suboptions: + configure_dnac_ip: + description: Configuration Cisco Catalyst Center IP for SNMP Server (eg true). + type: bool + ip_addresses: + description: IP Address for SNMP Server (eg 4.4.4.1). + elements: str + type: list + type: dict + syslog_server: + description: Network V2's syslogServer. + suboptions: + configure_dnac_ip: + description: Configuration Cisco Catalyst Center IP for syslog server (eg true). + type: bool + ip_addresses: + description: IP Address for syslog server (eg 4.4.4.4). + elements: str + type: list + type: dict + timezone: + description: Input for time zone (eg Africa/Abidjan). + type: str + site_name: + description: Site name path parameter. + type: str +requirements: +- dnacentersdk == 2.4.5 +- python >= 3.5 +notes: + - SDK Method used are + network_settings.NetworkSettings.create_global_pool, + network_settings.NetworkSettings.delete_global_ip_pool, + network_settings.NetworkSettings.update_global_pool, + network_settings.NetworkSettings.release_reserve_ip_subpool, + network_settings.NetworkSettings.reserve_ip_subpool, + network_settings.NetworkSettings.update_reserve_ip_subpool, + network_settings.NetworkSettings.update_network_v2, + + - Paths used are + post /dna/intent/api/v1/global-pool, + delete /dna/intent/api/v1/global-pool/{id}, + put /dna/intent/api/v1/global-pool, + post /dna/intent/api/v1/reserve-ip-subpool/{siteId}, + delete /dna/intent/api/v1/reserve-ip-subpool/{id}, + put /dna/intent/api/v1/reserve-ip-subpool/{siteId}, + put /dna/intent/api/v2/network/{siteId}, + +""" + +EXAMPLES = r""" +- name: Create global pool, reserve an ip pool and network + cisco.dnac.network_settings_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - global_pool_details: + settings: + ip_pool: + - name: string + gateway: string + ip_address_space: string + cidr: string + type: Generic + dhcp_server_ips: list + dns_server_ips: list + reserve_pool_details: + ipv6_address_space: True + ipv4_global_pool: string + ipv4_prefix: True + ipv4_prefix_length: 9 + ipv4_subnet: string + name: string + ipv6_prefix: True + ipv6_prefix_length: 64 + ipv6_global_pool: string + ipv6_subnet: string + site_name: string + slaac_support: True + type: LAN + network_management_details: + settings: + dhcp_server: list + dns_server: + domain_name: string + primary_ip_address: string + secondary_ip_address: string + client_and_endpoint_aaa: + network: string + protocol: string + servers: string + message_of_the_day: + banner_message: string + retain_existing_banner: string + netflow_collector: + ip_address: string + port: 443 + network_aaa: + network: string + protocol: string + servers: string + ntp_server: list + snmp_server: + configure_dnac_ip: True + ip_addresses: list + syslog_server: + configure_dnac_ip: True + ip_addresses: list + site_name: string +""" + +RETURN = r""" +# Case_1: Successful creation/updation/deletion of global pool +response_1: + description: A dictionary or list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "executionId": "string", + "executionStatusUrl": "string", + "message": "string" + } + +# Case_2: Successful creation/updation/deletion of reserve pool +response_2: + description: A dictionary or list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "executionId": "string", + "executionStatusUrl": "string", + "message": "string" + } + +# Case_3: Successful creation/updation of network +response_3: + description: A dictionary or list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "executionId": "string", + "executionStatusUrl": "string", + "message": "string" + } +""" + +import copy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, + get_dict_result, + dnac_compare_equality, +) + + +class NetworkSettings(DnacBase): + """Class containing member attributes for network intent module""" + + def __init__(self, module): + super().__init__(module) + self.result["response"] = [ + {"globalPool": {"response": {}, "msg": {}}}, + {"reservePool": {"response": {}, "msg": {}}}, + {"network": {"response": {}, "msg": {}}} + ] + self.global_pool_obj_params = self.get_obj_params("GlobalPool") + self.reserve_pool_obj_params = self.get_obj_params("ReservePool") + self.network_obj_params = self.get_obj_params("Network") + + def validate_input(self): + """ + Checks if the configuration parameters provided in the playbook + meet the expected structure and data types, + as defined in the 'temp_spec' dictionary. + + Parameters: + None + + Returns: + self + + """ + + if not self.config: + self.msg = "config not available in playbook for validation" + self.status = "success" + return self + + # temp_spec is the specification for the expected structure of configuration parameters + temp_spec = { + "global_pool_details": { + "type": 'dict', + "settings": { + "type": 'dict', + "ip_pool": { + "type": 'list', + "ip_address_space": {"type": 'string'}, + "dhcp_server_ips": {"type": 'list'}, + "dns_server_ips": {"type": 'list'}, + "gateway": {"type": 'string'}, + "cidr": {"type": 'string'}, + "name": {"type": 'string'}, + "prevName": {"type": 'string'}, + } + } + }, + "reserve_pool_details": { + "type": 'dict', + "name": {"type": 'string'}, + "prevName": {"type": 'string'}, + "ipv6_address_space": {"type": 'bool'}, + "ipv4_global_pool": {"type": 'string'}, + "ipv4_prefix": {"type": 'bool'}, + "ipv4_prefix_length": {"type": 'string'}, + "ipv4_subnet": {"type": 'string'}, + "ipv4GateWay": {"type": 'string'}, + "ipv4DhcpServers": {"type": 'list'}, + "ipv4_dns_servers": {"type": 'list'}, + "ipv6_global_pool": {"type": 'string'}, + "ipv6_prefix": {"type": 'bool'}, + "ipv6_prefix_length": {"type": 'integer'}, + "ipv6_subnet": {"type": 'string'}, + "ipv6GateWay": {"type": 'string'}, + "ipv6DhcpServers": {"type": 'list'}, + "ipv6DnsServers": {"type": 'list'}, + "ipv4TotalHost": {"type": 'integer'}, + "ipv6TotalHost": {"type": 'integer'}, + "slaac_support": {"type": 'bool'}, + "site_name": {"type": 'string'}, + }, + "network_management_details": { + "type": 'dict', + "settings": { + "type": 'dict', + "dhcp_server": {"type": 'list'}, + "dns_server": { + "type": 'dict', + "domain_name": {"type": 'string'}, + "primary_ip_address": {"type": 'string'}, + "secondary_ip_address": {"type": 'string'} + }, + "syslog_server": { + "type": 'dict', + "ip_addresses": {"type": 'list'}, + "configure_dnac_ip": {"type": 'bool'} + }, + "snmp_server": { + "type": 'dict', + "ip_addresses": {"type": 'list'}, + "configure_dnac_ip": {"type": 'bool'} + }, + "netflow_collector": { + "type": 'dict', + "ip_address": {"type": 'string'}, + "port": {"type": 'integer'}, + }, + "timezone": {"type": 'string'}, + "ntp_server": {"type": 'list'}, + "message_of_the_day": { + "type": 'dict', + "banner_message": {"type": 'string'}, + "retain_existing_banner": {"type": 'bool'}, + }, + "network_aaa": { + "type": 'dict', + "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, + "ip_address": {"type": 'string'}, + "network": {"type": 'string'}, + "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, + "shared_secret": {"type": 'string'} + + }, + "client_and_endpoint_aaa": { + "type": 'dict', + "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, + "ip_address": {"type": 'string'}, + "network": {"type": 'string'}, + "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, + "shared_secret": {"type": 'string'} + } + }, + "site_name": {"type": 'string'}, + } + } + + # Validate playbook params against the specification (temp_spec) + valid_temp, invalid_params = validate_list_of_dicts(self.config, temp_spec) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format("\n".join(invalid_params)) + self.status = "failed" + return self + + self.validated_config = valid_temp + self.log("Successfully validated playbook config params: {0}".format(valid_temp), "INFO") + self.msg = "Successfully validated input from the playbook" + self.status = "success" + return self + + def requires_update(self, have, want, obj_params): + """ + Check if the template config given requires update by comparing + current information wih the requested information. + + This method compares the current global pool, reserve pool, + or network details from Cisco Catalyst Center with the user-provided details + from the playbook, using a specified schema for comparison. + + Parameters: + have (dict) - Current information from the Cisco Catalyst Center + (global pool, reserve pool, network details) + want (dict) - Users provided information from the playbook + obj_params (list of tuples) - A list of parameter mappings specifying which + Cisco Catalyst Center parameters (dnac_param) correspond to + the user-provided parameters (ansible_param). + + Returns: + bool - True if any parameter specified in obj_params differs between + current_obj and requested_obj, indicating that an update is required. + False if all specified parameters are equal. + + """ + + current_obj = have + requested_obj = want + self.log("Current State (have): {0}".format(current_obj), "DEBUG") + self.log("Desired State (want): {0}".format(requested_obj), "DEBUG") + + return any(not dnac_compare_equality(current_obj.get(dnac_param), + requested_obj.get(ansible_param)) + for (dnac_param, ansible_param) in obj_params) + + def get_obj_params(self, get_object): + """ + Get the required comparison obj_params value + + Parameters: + get_object (str) - identifier for the required obj_params + + Returns: + obj_params (list) - obj_params value for comparison. + """ + + try: + if get_object == "GlobalPool": + obj_params = [ + ("settings", "settings"), + ] + elif get_object == "ReservePool": + obj_params = [ + ("name", "name"), + ("type", "type"), + ("ipv6AddressSpace", "ipv6AddressSpace"), + ("ipv4GlobalPool", "ipv4GlobalPool"), + ("ipv4Prefix", "ipv4Prefix"), + ("ipv4PrefixLength", "ipv4PrefixLength"), + ("ipv4GateWay", "ipv4GateWay"), + ("ipv4DhcpServers", "ipv4DhcpServers"), + ("ipv4DnsServers", "ipv4DnsServers"), + ("ipv6GateWay", "ipv6GateWay"), + ("ipv6DhcpServers", "ipv6DhcpServers"), + ("ipv6DnsServers", "ipv6DnsServers"), + ("ipv4TotalHost", "ipv4TotalHost"), + ("slaacSupport", "slaacSupport") + ] + elif get_object == "Network": + obj_params = [ + ("settings", "settings"), + ("site_name", "site_name") + ] + else: + raise ValueError("Received an unexpected value for 'get_object': {0}" + .format(get_object)) + except Exception as msg: + self.log("Received exception: {0}".format(msg), "CRITICAL") + + return obj_params + + def get_site_id(self, site_name): + """ + Get the site id from the site name. + Use check_return_status() to check for failure + + Parameters: + site_name (str) - Site name + + Returns: + str or None - The Site Id if found, or None if not found or error + """ + + try: + response = self.dnac._exec( + family="sites", + function='get_site', + params={"name": site_name}, + ) + self.log("Received API response from 'get_site': {0}".format(response), "DEBUG") + if not response: + self.log("Failed to retrieve the site ID for the site name: {0}" + .format(site_name), "ERROR") + return None + + _id = response.get("response")[0].get("id") + self.log("Site ID for site name '{0}': {1}".format(site_name, _id), "DEBUG") + except Exception as msg: + self.log("Exception occurred while retrieving site_id from the site_name: {0}" + .format(msg), "CRITICAL") + return None + + return _id + + def get_global_pool_params(self, pool_info): + """ + Process Global Pool params from playbook data for Global Pool config in Cisco Catalyst Center + + Parameters: + pool_info (dict) - Playbook data containing information about the global pool + + Returns: + dict or None - Processed Global Pool data in a format suitable + for Cisco Catalyst Center configuration, or None if pool_info is empty. + """ + + if not pool_info: + self.log("Global Pool is empty", "INFO") + return None + + self.log("Global Pool Details: {0}".format(pool_info), "DEBUG") + global_pool = { + "settings": { + "ippool": [{ + "dhcpServerIps": pool_info.get("dhcpServerIps"), + "dnsServerIps": pool_info.get("dnsServerIps"), + "ipPoolCidr": pool_info.get("ipPoolCidr"), + "ipPoolName": pool_info.get("ipPoolName"), + "type": pool_info.get("ipPoolType").capitalize() + }] + } + } + self.log("Formated global pool details: {0}".format(global_pool), "DEBUG") + global_ippool = global_pool.get("settings").get("ippool")[0] + if pool_info.get("ipv6") is False: + global_ippool.update({"IpAddressSpace": "IPv4"}) + else: + global_ippool.update({"IpAddressSpace": "IPv6"}) + + self.log("ip_address_space: {0}".format(global_ippool.get("IpAddressSpace")), "DEBUG") + if not pool_info["gateways"]: + global_ippool.update({"gateway": ""}) + else: + global_ippool.update({"gateway": pool_info.get("gateways")[0]}) + + return global_pool + + def get_reserve_pool_params(self, pool_info): + """ + Process Reserved Pool parameters from playbook data + for Reserved Pool configuration in Cisco Catalyst Center + + Parameters: + pool_info (dict) - Playbook data containing information about the reserved pool + + Returns: + reserve_pool (dict) - Processed Reserved pool data + in the format suitable for the Cisco Catalyst Center config + """ + + reserve_pool = { + "name": pool_info.get("groupName"), + "site_id": pool_info.get("siteId"), + } + if len(pool_info.get("ipPools")) == 1: + reserve_pool.update({ + "ipv4DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps"), + "ipv4DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), + "ipv6AddressSpace": "False" + }) + if pool_info.get("ipPools")[0].get("gateways") != []: + reserve_pool.update({"ipv4GateWay": pool_info.get("ipPools")[0].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"ipv6AddressSpace": "False"}) + elif len(pool_info.get("ipPools")) == 2: + if not pool_info.get("ipPools")[0].get("ipv6"): + reserve_pool.update({ + "ipv4DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps"), + "ipv4DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), + "ipv6AddressSpace": "True", + "ipv6DhcpServers": pool_info.get("ipPools")[1].get("dhcpServerIps"), + "ipv6DnsServers": pool_info.get("ipPools")[1].get("dnsServerIps"), + + }) + + if pool_info.get("ipPools")[0].get("gateways") != []: + reserve_pool.update({"ipv4GateWay": + pool_info.get("ipPools")[0].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + + if pool_info.get("ipPools")[1].get("gateways") != []: + reserve_pool.update({"ipv6GateWay": + pool_info.get("ipPools")[1].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + + elif not pool_info.get("ipPools")[1].get("ipv6"): + reserve_pool.update({ + "ipv4DhcpServers": pool_info.get("ipPools")[1].get("dhcpServerIps"), + "ipv4DnsServers": pool_info.get("ipPools")[1].get("dnsServerIps"), + "ipv6AddressSpace": "True", + "ipv6DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), + "ipv6DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps") + }) + if pool_info.get("ipPools")[1].get("gateways") != []: + reserve_pool.update({"ipv4GateWay": + pool_info.get("ipPools")[1].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + + if pool_info.get("ipPools")[0].get("gateways") != []: + reserve_pool.update({"ipv6GateWay": + pool_info.get("ipPools")[0].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"slaacSupport": True}) + self.log("Formatted reserve pool details: {0}".format(reserve_pool), "DEBUG") + return reserve_pool + + def get_network_params(self, site_id): + """ + Process the Network parameters from the playbook + for Network configuration in Cisco Catalyst Center + + Parameters: + site_id (str) - The Site ID for which network parameters are requested + + Returns: + dict or None: Processed Network data in a format + suitable for Cisco Catalyst Center configuration, or None + if the response is not a dictionary or there was an error. + """ + + response = self.dnac._exec( + family="network_settings", + function='get_network_v2', + params={"site_id": site_id} + ) + self.log("Received API response from 'get_network_v2': {0}".format(response), "DEBUG") + if not isinstance(response, dict): + self.log("Failed to retrieve the network details - " + "Response is not a dictionary", "ERROR") + return None + + # Extract various network-related details from the response + all_network_details = response.get("response") + dhcp_details = get_dict_result(all_network_details, "key", "dhcp.server") + dns_details = get_dict_result(all_network_details, "key", "dns.server") + snmp_details = get_dict_result(all_network_details, "key", "snmp.trap.receiver") + syslog_details = get_dict_result(all_network_details, "key", "syslog.server") + netflow_details = get_dict_result(all_network_details, "key", "netflow.collector") + ntpserver_details = get_dict_result(all_network_details, "key", "ntp.server") + timezone_details = get_dict_result(all_network_details, "key", "timezone.site") + messageoftheday_details = get_dict_result(all_network_details, "key", "device.banner") + network_aaa = get_dict_result(all_network_details, "key", "aaa.network.server.1") + network_aaa2 = get_dict_result(all_network_details, "key", "aaa.network.server.2") + network_aaa_pan = get_dict_result(all_network_details, "key", "aaa.server.pan.network") + clientAndEndpoint_aaa = get_dict_result(all_network_details, "key", "aaa.endpoint.server.1") + clientAndEndpoint_aaa2 = get_dict_result(all_network_details, + "key", + "aaa.endpoint.server.2") + clientAndEndpoint_aaa_pan = \ + get_dict_result(all_network_details, "key", "aaa.server.pan.endpoint") + + # Prepare the network details for Cisco Catalyst Center configuration + network_details = { + "settings": { + "snmpServer": { + "configureDnacIP": snmp_details.get("value")[0].get("configureDnacIP"), + "ipAddresses": snmp_details.get("value")[0].get("ipAddresses"), + }, + "syslogServer": { + "configureDnacIP": syslog_details.get("value")[0].get("configureDnacIP"), + "ipAddresses": syslog_details.get("value")[0].get("ipAddresses"), + }, + "netflowcollector": { + "ipAddress": netflow_details.get("value")[0].get("ipAddress"), + "port": netflow_details.get("value")[0].get("port") + }, + "timezone": timezone_details.get("value")[0], + } + } + network_settings = network_details.get("settings") + if dhcp_details and dhcp_details.get("value") != []: + network_settings.update({"dhcpServer": dhcp_details.get("value")}) + else: + network_settings.update({"dhcpServer": [""]}) + + if dns_details is not None: + network_settings.update({ + "dnsServer": { + "domainName": dns_details.get("value")[0].get("domainName"), + "primaryIpAddress": dns_details.get("value")[0].get("primaryIpAddress"), + "secondaryIpAddress": dns_details.get("value")[0].get("secondaryIpAddress") + } + }) + + if ntpserver_details and ntpserver_details.get("value") != []: + network_settings.update({"ntpServer": ntpserver_details.get("value")}) + else: + network_settings.update({"ntpServer": [""]}) + + if messageoftheday_details is not None: + network_settings.update({ + "messageOfTheday": { + "bannerMessage": messageoftheday_details.get("value")[0].get("bannerMessage"), + } + }) + retain_existing_banner = messageoftheday_details.get("value")[0] \ + .get("retainExistingBanner") + if retain_existing_banner is True: + network_settings.get("messageOfTheday").update({ + "retainExistingBanner": "true" + }) + else: + network_settings.get("messageOfTheday").update({ + "retainExistingBanner": "false" + }) + + if network_aaa and network_aaa_pan: + aaa_pan_value = network_aaa_pan.get("value")[0] + aaa_value = network_aaa.get("value")[0] + if aaa_pan_value == "None": + network_settings.update({ + "network_aaa": { + "network": aaa_value.get("ipAddress"), + "protocol": aaa_value.get("protocol"), + "ipAddress": network_aaa2.get("value")[0].get("ipAddress"), + "servers": "AAA" + } + }) + else: + network_settings.update({ + "network_aaa": { + "network": aaa_value.get("ipAddress"), + "protocol": aaa_value.get("protocol"), + "ipAddress": aaa_pan_value, + "servers": "ISE" + } + }) + + if clientAndEndpoint_aaa and clientAndEndpoint_aaa_pan: + aaa_pan_value = clientAndEndpoint_aaa_pan.get("value")[0] + aaa_value = clientAndEndpoint_aaa.get("value")[0] + if aaa_pan_value == "None": + network_settings.update({ + "clientAndEndpoint_aaa": { + "network": aaa_value.get("ipAddress"), + "protocol": aaa_value.get("protocol"), + "ipAddress": clientAndEndpoint_aaa2.get("value")[0].get("ipAddress"), + "servers": "AAA" + } + }) + else: + network_settings.update({ + "clientAndEndpoint_aaa": { + "network": aaa_value.get("ipAddress"), + "protocol": aaa_value.get("protocol"), + "ipAddress": aaa_pan_value, + "servers": "ISE" + } + }) + + self.log("Formatted playbook network details: {0}".format(network_details), "DEBUG") + return network_details + + def global_pool_exists(self, name): + """ + Check if the Global Pool with the given name exists + + Parameters: + name (str) - The name of the Global Pool to check for existence + + Returns: + dict - A dictionary containing information about the Global Pool's existence: + - 'exists' (bool): True if the Global Pool exists, False otherwise. + - 'id' (str or None): The ID of the Global Pool if it exists, or None if it doesn't. + - 'details' (dict or None): Details of the Global Pool if it exists, else None. + """ + + global_pool = { + "exists": False, + "details": None, + "id": None + } + response = self.dnac._exec( + family="network_settings", + function="get_global_pool", + ) + if not isinstance(response, dict): + self.log("Failed to retrieve the global pool details - " + "Response is not a dictionary", "CRITICAL") + return global_pool + + all_global_pool_details = response.get("response") + global_pool_details = get_dict_result(all_global_pool_details, "ipPoolName", name) + self.log("Global ip pool name: {0}".format(name), "DEBUG") + self.log("Global pool details: {0}".format(global_pool_details), "DEBUG") + if not global_pool_details: + self.log("Global pool {0} does not exist".format(name), "INFO") + return global_pool + global_pool.update({"exists": True}) + global_pool.update({"id": global_pool_details.get("id")}) + global_pool["details"] = self.get_global_pool_params(global_pool_details) + + self.log("Formatted global pool details: {0}".format(global_pool), "DEBUG") + return global_pool + + def reserve_pool_exists(self, name, site_name): + """ + Check if the Reserved pool with the given name exists in a specific site + Use check_return_status() to check for failure + + Parameters: + name (str) - The name of the Reserved pool to check for existence. + site_name (str) - The name of the site where the Reserved pool is located. + + Returns: + dict - A dictionary containing information about the Reserved pool's existence: + - 'exists' (bool): True if the Reserved pool exists in the specified site, else False. + - 'id' (str or None): The ID of the Reserved pool if it exists, or None if it doesn't. + - 'details' (dict or None): Details of the Reserved pool if it exists, or else None. + """ + + reserve_pool = { + "exists": False, + "details": None, + "id": None, + "success": True + } + site_id = self.get_site_id(site_name) + self.log("Site ID for the site name {0}: {1}".format(site_name, site_id), "DEBUG") + if not site_id: + reserve_pool.update({"success": False}) + self.msg = "Failed to get the site id from the site name {0}".format(site_name) + self.status = "failed" + return reserve_pool + + response = self.dnac._exec( + family="network_settings", + function="get_reserve_ip_subpool", + params={"siteId": site_id} + ) + if not isinstance(response, dict): + reserve_pool.update({"success": False}) + self.msg = "Error in getting reserve pool - Response is not a dictionary" + self.status = "exited" + return reserve_pool + + all_reserve_pool_details = response.get("response") + reserve_pool_details = get_dict_result(all_reserve_pool_details, "groupName", name) + if not reserve_pool_details: + self.log("Reserved pool {0} does not exist in the site {1}" + .format(name, site_name), "DEBUG") + return reserve_pool + + reserve_pool.update({"exists": True}) + reserve_pool.update({"id": reserve_pool_details.get("id")}) + reserve_pool.update({"details": self.get_reserve_pool_params(reserve_pool_details)}) + + self.log("Reserved pool details: {0}".format(reserve_pool.get("details")), "DEBUG") + self.log("Reserved pool id: {0}".format(reserve_pool.get("id")), "DEBUG") + return reserve_pool + + def get_have_global_pool(self, config): + """ + Get the current Global Pool information from + Cisco Catalyst Center based on the provided playbook details. + check this API using check_return_status. + + Parameters: + config (dict) - Playbook details containing Global Pool configuration. + + Returns: + self - The current object with updated information. + """ + + global_pool = { + "exists": False, + "details": None, + "id": None + } + global_pool_settings = config.get("global_pool_details").get("settings") + if global_pool_settings is None: + self.msg = "settings in global_pool_details is missing in the playbook" + self.status = "failed" + return self + + global_pool_ippool = global_pool_settings.get("ip_pool") + if global_pool_ippool is None: + self.msg = "ip_pool in global_pool_details is missing in the playbook" + self.status = "failed" + return self + + name = global_pool_ippool[0].get("name") + if name is None: + self.msg = "Mandatory Parameter name required" + self.status = "failed" + return self + + # If the Global Pool doesn't exist and a previous name is provided + # Else try using the previous name + global_pool = self.global_pool_exists(name) + self.log("Global pool details: {0}".format(global_pool), "DEBUG") + prev_name = global_pool_ippool[0].get("prev_name") + if global_pool.get("exists") is False and \ + prev_name is not None: + global_pool = self.global_pool_exists(prev_name) + if global_pool.get("exists") is False: + self.msg = "Prev name {0} doesn't exist in global_pool_details".format(prev_name) + self.status = "failed" + return self + + self.log("Global pool exists: {0}".format(global_pool.get("exists")), "DEBUG") + self.log("Current Site: {0}".format(global_pool.get("details")), "DEBUG") + self.have.update({"globalPool": global_pool}) + self.msg = "Collecting the global pool details from the Cisco Catalyst Center" + self.status = "success" + return self + + def get_have_reserve_pool(self, config): + """ + Get the current Reserved Pool information from Cisco Catalyst Center + based on the provided playbook details. + Check this API using check_return_status + + Parameters: + config (list of dict) - Playbook details containing Reserved Pool configuration. + + Returns: + self - The current object with updated information. + """ + + reserve_pool = { + "exists": False, + "details": None, + "id": None + } + reserve_pool_details = config.get("reserve_pool_details") + name = reserve_pool_details.get("name") + if name is None: + self.msg = "Mandatory Parameter name required in reserve_pool_details\n" + self.status = "failed" + return self + + site_name = reserve_pool_details.get("site_name") + self.log("Site Name: {0}".format(site_name), "DEBUG") + if site_name is None: + self.msg = "Missing parameter 'site_name' in reserve_pool_details" + self.status = "failed" + return self + + # Check if the Reserved Pool exists in Cisco Catalyst Center + # based on the provided name and site name + reserve_pool = self.reserve_pool_exists(name, site_name) + if not reserve_pool.get("success"): + return self.check_return_status() + self.log("Reserved pool details: {0}".format(reserve_pool), "DEBUG") + + # If the Reserved Pool doesn't exist and a previous name is provided + # Else try using the previous name + prev_name = reserve_pool_details.get("prev_name") + if reserve_pool.get("exists") is False and \ + prev_name is not None: + reserve_pool = self.reserve_pool_exists(prev_name, site_name) + if not reserve_pool.get("success"): + return self.check_return_status() + + # If the previous name doesn't exist in Cisco Catalyst Center, return with error + if reserve_pool.get("exists") is False: + self.msg = "Prev name {0} doesn't exist in reserve_pool_details".format(prev_name) + self.status = "failed" + return self + + self.log("Reserved pool exists: {0}".format(reserve_pool.get("exists")), "DEBUG") + self.log("Reserved pool: {0}".format(reserve_pool.get("details")), "DEBUG") + + # If reserve pool exist, convert ipv6AddressSpace to the required format (boolean) + if reserve_pool.get("exists"): + reserve_pool_details = reserve_pool.get("details") + if reserve_pool_details.get("ipv6AddressSpace") == "False": + reserve_pool_details.update({"ipv6AddressSpace": False}) + else: + reserve_pool_details.update({"ipv6AddressSpace": True}) + + self.log("Reserved pool details: {0}".format(reserve_pool), "DEBUG") + self.have.update({"reservePool": reserve_pool}) + self.msg = "Collecting the reserve pool details from the Cisco Catalyst Center" + self.status = "success" + return self + + def get_have_network(self, config): + """ + Get the current Network details from Cisco Catalyst + Center based on the provided playbook details. + + Parameters: + config (dict) - Playbook details containing Network Management configuration. + + Returns: + self - The current object with updated Network information. + """ + network = {} + site_name = config.get("network_management_details").get("site_name") + if site_name is None: + self.msg = "Mandatory Parameter 'site_name' missing" + self.status = "failed" + return self + + site_id = self.get_site_id(site_name) + if site_id is None: + self.msg = "Failed to get site id from {0}".format(site_name) + self.status = "failed" + return self + + network["site_id"] = site_id + network["net_details"] = self.get_network_params(site_id) + self.log("Network details from the Catalyst Center: {0}".format(network), "DEBUG") + self.have.update({"network": network}) + self.msg = "Collecting the network details from the Cisco Catalyst Center" + self.status = "success" + return self + + def get_have(self, config): + """ + Get the current Global Pool Reserved Pool and Network details from Cisco Catalyst Center + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self - The current object with updated Global Pool, + Reserved Pool, and Network information. + """ + + if config.get("global_pool_details") is not None: + self.get_have_global_pool(config).check_return_status() + + if config.get("reserve_pool_details") is not None: + self.get_have_reserve_pool(config).check_return_status() + + if config.get("network_management_details") is not None: + self.get_have_network(config).check_return_status() + + self.log("Current State (have): {0}".format(self.have), "INFO") + self.msg = "Successfully retrieved the details from the Cisco Catalyst Center" + self.status = "success" + return self + + def get_want_global_pool(self, global_ippool): + """ + Get all the Global Pool information from playbook + Set the status and the msg before returning from the API + Check the return value of the API with check_return_status() + + Parameters: + global_ippool (dict) - Playbook global pool details containing IpAddressSpace, + DHCP server IPs, DNS server IPs, IP pool name, IP pool CIDR, gateway, and type. + + Returns: + self - The current object with updated desired Global Pool information. + """ + + # Initialize the desired Global Pool configuration + want_global = { + "settings": { + "ippool": [{ + "IpAddressSpace": global_ippool.get("ip_address_space"), + "dhcpServerIps": global_ippool.get("dhcp_server_ips"), + "dnsServerIps": global_ippool.get("dns_server_ips"), + "ipPoolName": global_ippool.get("name"), + "ipPoolCidr": global_ippool.get("cidr"), + "gateway": global_ippool.get("gateway"), + "type": global_ippool.get("type"), + }] + } + } + want_ippool = want_global.get("settings").get("ippool")[0] + + # Converting to the required format based on the existing Global Pool + if not self.have.get("globalPool").get("exists"): + if want_ippool.get("dhcpServerIps") is None: + want_ippool.update({"dhcpServerIps": []}) + if want_ippool.get("dnsServerIps") is None: + want_ippool.update({"dnsServerIps": []}) + if want_ippool.get("IpAddressSpace") is None: + want_ippool.update({"IpAddressSpace": ""}) + if want_ippool.get("gateway") is None: + want_ippool.update({"gateway": ""}) + if want_ippool.get("type") is None: + want_ippool.update({"type": "Generic"}) + else: + have_ippool = self.have.get("globalPool").get("details") \ + .get("settings").get("ippool")[0] + + # Copy existing Global Pool information if the desired configuration is not provided + want_ippool.update({ + "IpAddressSpace": have_ippool.get("IpAddressSpace"), + "type": have_ippool.get("type"), + "ipPoolCidr": have_ippool.get("ipPoolCidr") + }) + want_ippool.update({}) + want_ippool.update({}) + + for key in ["dhcpServerIps", "dnsServerIps", "gateway"]: + if want_ippool.get(key) is None and have_ippool.get(key) is not None: + want_ippool[key] = have_ippool[key] + + self.log("Global pool playbook details: {0}".format(want_global), "DEBUG") + self.want.update({"wantGlobal": want_global}) + self.msg = "Collecting the global pool details from the playbook" + self.status = "success" + return self + + def get_want_reserve_pool(self, reserve_pool): + """ + Get all the Reserved Pool information from playbook + Set the status and the msg before returning from the API + Check the return value of the API with check_return_status() + + Parameters: + reserve_pool (dict) - Playbook reserved pool + details containing various properties. + + Returns: + self - The current object with updated desired Reserved Pool information. + """ + + want_reserve = { + "name": reserve_pool.get("name"), + "type": reserve_pool.get("type"), + "ipv6AddressSpace": reserve_pool.get("ipv6_address_space"), + "ipv4GlobalPool": reserve_pool.get("ipv4_global_pool"), + "ipv4Prefix": reserve_pool.get("ipv4_prefix"), + "ipv4PrefixLength": reserve_pool.get("ipv4_prefix_length"), + "ipv4GateWay": reserve_pool.get("ipv4GateWay"), + "ipv4DhcpServers": reserve_pool.get("ipv4DhcpServers"), + "ipv4DnsServers": reserve_pool.get("ipv4_dns_servers"), + "ipv4Subnet": reserve_pool.get("ipv4_subnet"), + "ipv6GlobalPool": reserve_pool.get("ipv6_global_pool"), + "ipv6Prefix": reserve_pool.get("ipv6_prefix"), + "ipv6PrefixLength": reserve_pool.get("ipv6_prefix_length"), + "ipv6GateWay": reserve_pool.get("ipv6GateWay"), + "ipv6DhcpServers": reserve_pool.get("ipv6DhcpServers"), + "ipv6Subnet": reserve_pool.get("ipv6_subnet"), + "ipv6DnsServers": reserve_pool.get("ipv6DnsServers"), + "ipv4TotalHost": reserve_pool.get("ipv4TotalHost"), + "ipv6TotalHost": reserve_pool.get("ipv6TotalHost") + } + + # Check for missing mandatory parameters in the playbook + if not want_reserve.get("name"): + self.msg = "Missing mandatory parameter 'name' in reserve_pool_details" + self.status = "failed" + return self + + if want_reserve.get("ipv4Prefix") is True: + if want_reserve.get("ipv4Subnet") is None and \ + want_reserve.get("ipv4TotalHost") is None: + self.msg = "missing parameter 'ipv4_subnet' or 'ipv4TotalHost' \ + while adding the ipv4 in reserve_pool_details" + self.status = "failed" + return self + + if want_reserve.get("ipv6Prefix") is True: + if want_reserve.get("ipv6Subnet") is None and \ + want_reserve.get("ipv6TotalHost") is None: + self.msg = "missing parameter 'ipv6_subnet' or 'ipv6TotalHost' \ + while adding the ipv6 in reserve_pool_details" + self.status = "failed" + return self + + self.log("Reserved IP pool playbook details: {0}".format(want_reserve), "DEBUG") + + # If there are no existing Reserved Pool details, validate and set defaults + if not self.have.get("reservePool").get("details"): + if not want_reserve.get("ipv4GlobalPool"): + self.msg = "missing parameter 'ipv4GlobalPool' in reserve_pool_details" + self.status = "failed" + return self + + if not want_reserve.get("ipv4PrefixLength"): + self.msg = "missing parameter 'ipv4_prefix_length' in reserve_pool_details" + self.status = "failed" + return self + + if want_reserve.get("type") is None: + want_reserve.update({"type": "Generic"}) + if want_reserve.get("ipv4GateWay") is None: + want_reserve.update({"ipv4GateWay": ""}) + if want_reserve.get("ipv4DhcpServers") is None: + want_reserve.update({"ipv4DhcpServers": []}) + if want_reserve.get("ipv4DnsServers") is None: + want_reserve.update({"ipv4DnsServers": []}) + if want_reserve.get("ipv6AddressSpace") is None: + want_reserve.update({"ipv6AddressSpace": False}) + if want_reserve.get("slaacSupport") is None: + want_reserve.update({"slaacSupport": True}) + if want_reserve.get("ipv4TotalHost") is None: + del want_reserve['ipv4TotalHost'] + if want_reserve.get("ipv6AddressSpace") is True: + want_reserve.update({"ipv6Prefix": True}) + else: + del want_reserve['ipv6Prefix'] + + if not want_reserve.get("ipv6AddressSpace"): + keys_to_check = ['ipv6GlobalPool', 'ipv6PrefixLength', + 'ipv6GateWay', 'ipv6DhcpServers', + 'ipv6DnsServers', 'ipv6TotalHost'] + for key in keys_to_check: + if want_reserve.get(key) is None: + del want_reserve[key] + else: + keys_to_delete = ['type', 'ipv4GlobalPool', + 'ipv4Prefix', 'ipv4PrefixLength', + 'ipv4TotalHost', 'ipv4Subnet'] + for key in keys_to_delete: + if key in want_reserve: + del want_reserve[key] + + self.want.update({"wantReserve": want_reserve}) + self.log("Desired State (want): {0}".format(self.want), "INFO") + self.msg = "Collecting the reserve pool details from the playbook" + self.status = "success" + return self + + def get_want_network(self, network_management_details): + """ + Get all the Network related information from playbook + Set the status and the msg before returning from the API + Check the return value of the API with check_return_status() + + Parameters: + network_management_details (dict) - Playbook network + details containing various network settings. + + Returns: + self - The current object with updated desired Network-related information. + """ + + want_network = { + "settings": { + "dhcpServer": {}, + "dnsServer": {}, + "snmpServer": {}, + "syslogServer": {}, + "netflowcollector": {}, + "ntpServer": {}, + "timezone": "", + "messageOfTheday": {}, + "network_aaa": {}, + "clientAndEndpoint_aaa": {} + } + } + want_network_settings = want_network.get("settings") + self.log("Current state (have): {0}".format(self.have), "DEBUG") + if network_management_details.get("dhcp_server") is not None: + want_network_settings.update({ + "dhcpServer": network_management_details.get("dhcp_server") + }) + else: + del want_network_settings["dhcpServer"] + + if network_management_details.get("ntp_server") is not None: + want_network_settings.update({ + "ntpServer": network_management_details.get("ntp_server") + }) + else: + del want_network_settings["ntpServer"] + + if network_management_details.get("timezone") is not None: + want_network_settings["timezone"] = \ + network_management_details.get("timezone") + else: + self.msg = "missing parameter timezone in network" + self.status = "failed" + return self + + dnsServer = network_management_details.get("dns_server") + if dnsServer is not None: + if dnsServer.get("domain_name") is not None: + want_network_settings.get("dnsServer").update({ + "domainName": + dnsServer.get("domain_name") + }) + + if dnsServer.get("primary_ip_address") is not None: + want_network_settings.get("dnsServer").update({ + "primaryIpAddress": + dnsServer.get("primary_ip_address") + }) + + if dnsServer.get("secondary_ip_address") is not None: + want_network_settings.get("dnsServer").update({ + "secondaryIpAddress": + dnsServer.get("secondary_ip_address") + }) + else: + del want_network_settings["dnsServer"] + + snmpServer = network_management_details.get("snmp_server") + if snmpServer is not None: + if snmpServer.get("configure_dnac_ip") is not None: + want_network_settings.get("snmpServer").update({ + "configureDnacIP": snmpServer.get("configure_dnac_ip") + }) + if snmpServer.get("ip_addresses") is not None: + want_network_settings.get("snmpServer").update({ + "ipAddresses": snmpServer.get("ip_addresses") + }) + else: + del want_network_settings["snmpServer"] + + syslogServer = network_management_details.get("syslog_server") + if syslogServer is not None: + if syslogServer.get("configure_dnac_ip") is not None: + want_network_settings.get("syslogServer").update({ + "configureDnacIP": syslogServer.get("configure_dnac_ip") + }) + if syslogServer.get("ip_addresses") is not None: + want_network_settings.get("syslogServer").update({ + "ipAddresses": syslogServer.get("ip_addresses") + }) + else: + del want_network_settings["syslogServer"] + + netflowcollector = network_management_details.get("netflow_collector") + if netflowcollector is not None: + if netflowcollector.get("ip_address") is not None: + want_network_settings.get("netflowcollector").update({ + "ipAddress": + netflowcollector.get("ip_address") + }) + if netflowcollector.get("port") is not None: + want_network_settings.get("netflowcollector").update({ + "port": + netflowcollector.get("port") + }) + else: + del want_network_settings["netflowcollector"] + + messageOfTheday = network_management_details.get("message_of_the_day") + if messageOfTheday is not None: + if messageOfTheday.get("banner_message") is not None: + want_network_settings.get("messageOfTheday").update({ + "bannerMessage": + messageOfTheday.get("banner_message") + }) + if messageOfTheday.get("retain_existing_banner") is not None: + want_network_settings.get("messageOfTheday").update({ + "retainExistingBanner": + messageOfTheday.get("retain_existing_banner") + }) + else: + del want_network_settings["messageOfTheday"] + + network_aaa = network_management_details.get("network_aaa") + if network_aaa: + if network_aaa.get("ip_address"): + want_network_settings.get("network_aaa").update({ + "ipAddress": + network_aaa.get("ip_address") + }) + else: + if network_aaa.get("servers") == "ISE": + self.msg = "missing parameter ip_address in network_aaa, server ISE is set" + self.status = "failed" + return self + + if network_aaa.get("network"): + want_network_settings.get("network_aaa").update({ + "network": network_aaa.get("network") + }) + else: + self.msg = "missing parameter network in network_aaa" + self.status = "failed" + return self + + if network_aaa.get("protocol"): + want_network_settings.get("network_aaa").update({ + "protocol": + network_aaa.get("protocol") + }) + else: + self.msg = "missing parameter protocol in network_aaa" + self.status = "failed" + return self + + if network_aaa.get("servers"): + want_network_settings.get("network_aaa").update({ + "servers": + network_aaa.get("servers") + }) + else: + self.msg = "missing parameter servers in network_aaa" + self.status = "failed" + return self + + if network_aaa.get("shared_secret"): + want_network_settings.get("network_aaa").update({ + "sharedSecret": + network_aaa.get("shared_secret") + }) + else: + del want_network_settings["network_aaa"] + + clientAndEndpoint_aaa = network_management_details.get("client_and_endpoint_aaa") + if clientAndEndpoint_aaa: + if clientAndEndpoint_aaa.get("ip_address"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "ipAddress": + clientAndEndpoint_aaa.get("ip_address") + }) + else: + if clientAndEndpoint_aaa.get("servers") == "ISE": + self.msg = "missing parameter ip_address in clientAndEndpoint_aaa, \ + server ISE is set" + self.status = "failed" + return self + + if clientAndEndpoint_aaa.get("network"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "network": + clientAndEndpoint_aaa.get("network") + }) + else: + self.msg = "missing parameter network in clientAndEndpoint_aaa" + self.status = "failed" + return self + + if clientAndEndpoint_aaa.get("protocol"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "protocol": + clientAndEndpoint_aaa.get("protocol") + }) + else: + self.msg = "missing parameter protocol in clientAndEndpoint_aaa" + self.status = "failed" + return self + + if clientAndEndpoint_aaa.get("servers"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "servers": + clientAndEndpoint_aaa.get("servers") + }) + else: + self.msg = "missing parameter servers in clientAndEndpoint_aaa" + self.status = "failed" + return self + + if clientAndEndpoint_aaa.get("shared_secret"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "sharedSecret": + clientAndEndpoint_aaa.get("shared_secret") + }) + else: + del want_network_settings["clientAndEndpoint_aaa"] + + self.log("Network playbook details: {0}".format(want_network), "DEBUG") + self.want.update({"wantNetwork": want_network}) + self.msg = "Collecting the network details from the playbook" + self.status = "success" + return self + + def get_want(self, config): + """ + Get all the Global Pool Reserved Pool and Network related information from playbook + + Parameters: + config (list of dict) - Playbook details + + Returns: + None + """ + + if config.get("global_pool_details"): + global_ippool = config.get("global_pool_details").get("settings").get("ip_pool")[0] + self.get_want_global_pool(global_ippool).check_return_status() + + if config.get("reserve_pool_details"): + reserve_pool = config.get("reserve_pool_details") + self.get_want_reserve_pool(reserve_pool).check_return_status() + + if config.get("network_management_details"): + network_management_details = config.get("network_management_details") \ + .get("settings") + self.get_want_network(network_management_details).check_return_status() + + self.log("Desired State (want): {0}".format(self.want), "INFO") + self.msg = "Successfully retrieved details from the playbook" + self.status = "success" + return self + + def update_global_pool(self, config): + """ + Update/Create Global Pool in Cisco Catalyst Center with fields provided in playbook + + Parameters: + config (list of dict) - Playbook details + + Returns: + None + """ + + name = config.get("global_pool_details") \ + .get("settings").get("ip_pool")[0].get("name") + result_global_pool = self.result.get("response")[0].get("globalPool") + result_global_pool.get("response").update({name: {}}) + + # Check pool exist, if not create and return + if not self.have.get("globalPool").get("exists"): + pool_params = self.want.get("wantGlobal") + self.log("Desired State for global pool (want): {0}".format(pool_params), "DEBUG") + response = self.dnac._exec( + family="network_settings", + function="create_global_pool", + params=pool_params, + ) + self.check_execution_response_status(response).check_return_status() + self.log("Successfully created global pool '{0}'.".format(name), "INFO") + result_global_pool.get("response").get(name) \ + .update({"globalPool Details": self.want.get("wantGlobal")}) + result_global_pool.get("msg").update({name: "Global Pool Created Successfully"}) + return + + # Pool exists, check update is required + if not self.requires_update(self.have.get("globalPool").get("details"), + self.want.get("wantGlobal"), self.global_pool_obj_params): + self.log("Global pool '{0}' doesn't require an update".format(name), "INFO") + result_global_pool.get("response").get(name).update({ + "Cisco Catalyst Center params": + self.have.get("globalPool").get("details").get("settings").get("ippool")[0] + }) + result_global_pool.get("response").get(name).update({ + "Id": self.have.get("globalPool").get("id") + }) + result_global_pool.get("msg").update({ + name: "Global pool doesn't require an update" + }) + return + + self.log("Global pool requires update", "DEBUG") + # Pool Exists + pool_params = copy.deepcopy(self.want.get("wantGlobal")) + pool_params_ippool = pool_params.get("settings").get("ippool")[0] + pool_params_ippool.update({"id": self.have.get("globalPool").get("id")}) + self.log("Desired State for global pool (want): {0}".format(pool_params), "DEBUG") + keys_to_remove = ["IpAddressSpace", "ipPoolCidr", "type"] + for key in keys_to_remove: + del pool_params["settings"]["ippool"][0][key] + + have_ippool = self.have.get("globalPool").get("details").get("settings").get("ippool")[0] + keys_to_update = ["dhcpServerIps", "dnsServerIps", "gateway"] + for key in keys_to_update: + if pool_params_ippool.get(key) is None: + pool_params_ippool[key] = have_ippool.get(key) + + self.log("Desired global pool details (want): {0}".format(pool_params), "DEBUG") + response = self.dnac._exec( + family="network_settings", + function="update_global_pool", + params=pool_params, + ) + + self.check_execution_response_status(response).check_return_status() + self.log("Global pool '{0}' updated successfully".format(name), "INFO") + result_global_pool.get("response").get(name) \ + .update({"Id": self.have.get("globalPool").get("details").get("id")}) + result_global_pool.get("msg").update({name: "Global Pool Updated Successfully"}) + return + + def update_reserve_pool(self, config): + """ + Update or Create a Reserve Pool in Cisco Catalyst Center based on the provided configuration. + This method checks if a reserve pool with the specified name exists in Cisco Catalyst Center. + If it exists and requires an update, it updates the pool. If not, it creates a new pool. + + Parameters: + config (list of dict) - Playbook details containing Reserve Pool information. + + Returns: + None + """ + + name = config.get("reserve_pool_details").get("name") + result_reserve_pool = self.result.get("response")[1].get("reservePool") + result_reserve_pool.get("response").update({name: {}}) + self.log("Current reserved pool details in Catalyst Center: {0}" + .format(self.have.get("reservePool").get("details")), "DEBUG") + self.log("Desired reserved pool details in Catalyst Center: {0}" + .format(self.want.get("wantReserve")), "DEBUG") + + # Check pool exist, if not create and return + self.log("IPv4 global pool: {0}" + .format(self.want.get("wantReserve").get("ipv4GlobalPool")), "DEBUG") + site_name = config.get("reserve_pool_details").get("site_name") + reserve_params = self.want.get("wantReserve") + site_id = self.get_site_id(site_name) + reserve_params.update({"site_id": site_id}) + if not self.have.get("reservePool").get("exists"): + self.log("Desired reserved pool details (want): {0}".format(reserve_params), "DEBUG") + response = self.dnac._exec( + family="network_settings", + function="reserve_ip_subpool", + params=reserve_params, + ) + self.check_execution_response_status(response).check_return_status() + self.log("Successfully created IP subpool reservation '{0}'.".format(name), "INFO") + result_reserve_pool.get("response").get(name) \ + .update({"reservePool Details": self.want.get("wantReserve")}) + result_reserve_pool.get("msg") \ + .update({name: "Ip Subpool Reservation Created Successfully"}) + return + + # Check update is required + if not self.requires_update(self.have.get("reservePool").get("details"), + self.want.get("wantReserve"), self.reserve_pool_obj_params): + self.log("Reserved ip subpool '{0}' doesn't require an update".format(name), "INFO") + result_reserve_pool.get("response").get(name) \ + .update({"Cisco Catalyst Center params": self.have.get("reservePool").get("details")}) + result_reserve_pool.get("response").get(name) \ + .update({"Id": self.have.get("reservePool").get("id")}) + result_reserve_pool.get("msg") \ + .update({name: "Reserve ip subpool doesn't require an update"}) + return + + self.log("Reserved ip pool '{0}' requires an update".format(name), "DEBUG") + # Pool Exists + self.log("Current reserved ip pool '{0}' details in Catalyst Center: {1}" + .format(name, self.have.get("reservePool")), "DEBUG") + self.log("Desired reserved ip pool '{0}' details: {1}" + .format(name, self.want.get("wantReserve")), "DEBUG") + reserve_params.update({"id": self.have.get("reservePool").get("id")}) + response = self.dnac._exec( + family="network_settings", + function="update_reserve_ip_subpool", + params=reserve_params, + ) + self.check_execution_response_status(response).check_return_status() + self.log("Reserved ip subpool '{0}' updated successfully.".format(name), "INFO") + result_reserve_pool['msg'] = "Reserved Ip Subpool Updated Successfully" + result_reserve_pool.get("response").get(name) \ + .update({"Reservation details": self.have.get("reservePool").get("details")}) + return + + def update_network(self, config): + """ + Update or create a network configuration in Cisco Catalyst + Center based on the provided playbook details. + + Parameters: + config (list of dict) - Playbook details containing Network Management information. + + Returns: + None + """ + + site_name = config.get("network_management_details").get("site_name") + result_network = self.result.get("response")[2].get("network") + result_network.get("response").update({site_name: {}}) + + # Check update is required or not + if not self.requires_update(self.have.get("network").get("net_details"), + self.want.get("wantNetwork"), self.network_obj_params): + + self.log("Network in site '{0}' doesn't require an update.".format(site_name), "INFO") + result_network.get("response").get(site_name).update({ + "Cisco Catalyst Center params": self.have.get("network") + .get("net_details").get("settings") + }) + result_network.get("msg").update({site_name: "Network doesn't require an update"}) + return + + self.log("Network in site '{0}' requires update.".format(site_name), "INFO") + self.log("Current State of network in Catalyst Center: {0}" + .format(self.have.get("network")), "DEBUG") + self.log("Desired State of network: {0}".format(self.want.get("wantNetwork")), "DEBUG") + + net_params = copy.deepcopy(self.want.get("wantNetwork")) + net_params.update({"site_id": self.have.get("network").get("site_id")}) + response = self.dnac._exec( + family="network_settings", + function='update_network_v2', + params=net_params, + ) + self.log("Received API response of 'update_network_v2': {0}".format(response), "DEBUG") + validation_string = "desired common settings operation successful" + self.check_task_response_status(response, validation_string).check_return_status() + self.log("Network has been changed successfully", "INFO") + result_network.get("msg") \ + .update({site_name: "Network Updated successfully"}) + result_network.get("response").get(site_name) \ + .update({"Network Details": self.want.get("wantNetwork").get("settings")}) + return + + def get_diff_merged(self, config): + """ + Update or create Global Pool, Reserve Pool, and + Network configurations in Cisco Catalyst Center based on the playbook details + + Parameters: + config (list of dict) - Playbook details containing + Global Pool, Reserve Pool, and Network Management information. + + Returns: + self + """ + + if config.get("global_pool_details") is not None: + self.update_global_pool(config) + + if config.get("reserve_pool_details") is not None: + self.update_reserve_pool(config) + + if config.get("network_management_details") is not None: + self.update_network(config) + + return self + + def delete_reserve_pool(self, name): + """ + Delete a Reserve Pool by name in Cisco Catalyst Center + + Parameters: + name (str) - The name of the Reserve Pool to be deleted. + + Returns: + self + """ + + reserve_pool_exists = self.have.get("reservePool").get("exists") + result_reserve_pool = self.result.get("response")[1].get("reservePool") + + if not reserve_pool_exists: + result_reserve_pool.get("response").update({name: "Reserve Pool not found"}) + self.msg = "Reserved Ip Subpool Not Found" + self.status = "success" + return self + + self.log("Reserved IP pool scheduled for deletion: {0}" + .format(self.have.get("reservePool").get("name")), "INFO") + _id = self.have.get("reservePool").get("id") + self.log("Reserved pool {0} id: {1}".format(name, _id), "DEBUG") + response = self.dnac._exec( + family="network_settings", + function="release_reserve_ip_subpool", + params={"id": _id}, + ) + self.check_execution_response_status(response).check_return_status() + executionid = response.get("executionId") + result_reserve_pool = self.result.get("response")[1].get("reservePool") + result_reserve_pool.get("response").update({name: {}}) + result_reserve_pool.get("response").get(name) \ + .update({"Execution Id": executionid}) + result_reserve_pool.get("msg") \ + .update({name: "Ip subpool reservation released successfully"}) + self.msg = "Reserved pool - {0} released successfully".format(name) + self.status = "success" + return self + + def delete_global_pool(self, name): + """ + Delete a Global Pool by name in Cisco Catalyst Center + + Parameters: + name (str) - The name of the Global Pool to be deleted. + + Returns: + self + """ + + global_pool_exists = self.have.get("globalPool").get("exists") + result_global_pool = self.result.get("response")[0].get("globalPool") + if not global_pool_exists: + result_global_pool.get("response").update({name: "Global Pool not found"}) + self.msg = "Global pool Not Found" + self.status = "success" + return self + + response = self.dnac._exec( + family="network_settings", + function="delete_global_ip_pool", + params={"id": self.have.get("globalPool").get("id")}, + ) + + # Check the execution status + self.check_execution_response_status(response).check_return_status() + executionid = response.get("executionId") + + # Update result information + result_global_pool = self.result.get("response")[0].get("globalPool") + result_global_pool.get("response").update({name: {}}) + result_global_pool.get("response").get(name).update({"Execution Id": executionid}) + result_global_pool.get("msg").update({name: "Pool deleted successfully"}) + self.msg = "Global pool - {0} deleted successfully".format(name) + self.status = "success" + return self + + def get_diff_deleted(self, config): + """ + Delete Reserve Pool and Global Pool in Cisco Catalyst Center based on playbook details. + + Parameters: + config (list of dict) - Playbook details + + Returns: + self + """ + + if config.get("reserve_pool_details") is not None: + name = config.get("reserve_pool_details").get("name") + self.delete_reserve_pool(name).check_return_status() + + if config.get("global_pool_details") is not None: + name = config.get("global_pool_details") \ + .get("settings").get("ip_pool")[0].get("name") + self.delete_global_pool(name).check_return_status() + + return self + + def verify_diff_merged(self, config): + """ + Validating the Cisco Catalyst Center configuration with the playbook details + when state is merged (Create/Update). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + self.get_have(config) + self.log("Current State (have): {0}".format(self.have), "INFO") + self.log("Requested State (want): {0}".format(self.want), "INFO") + if config.get("global_pool_details") is not None: + self.log("Desired State of global pool (want): {0}" + .format(self.want.get("wantGlobal")), "DEBUG") + self.log("Current State of global pool (have): {0}" + .format(self.have.get("globalPool").get("details")), "DEBUG") + if self.requires_update(self.have.get("globalPool").get("details"), + self.want.get("wantGlobal"), self.global_pool_obj_params): + self.msg = "Global Pool Config is not applied to the Cisco Catalyst Center" + self.status = "failed" + return self + + self.log("Successfully validated global pool '{0}'.".format(self.want + .get("wantGlobal").get("settings").get("ippool")[0].get("ipPoolName")), "INFO") + self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) + + if config.get("reserve_pool_details") is not None: + if self.requires_update(self.have.get("reservePool").get("details"), + self.want.get("wantReserve"), self.reserve_pool_obj_params): + self.log("Desired State for reserve pool (want): {0}" + .format(self.want.get("wantReserve")), "DEBUG") + self.log("Current State for reserve pool (have): {0}" + .format(self.have.get("reservePool").get("details")), "DEBUG") + self.msg = "Reserved Pool Config is not applied to the Cisco Catalyst Center" + self.status = "failed" + return self + + self.log("Successfully validated the reserved pool '{0}'." + .format(self.want.get("wantReserve").get("name")), "INFO") + self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) + + if config.get("network_management_details") is not None: + if self.requires_update(self.have.get("network").get("net_details"), + self.want.get("wantNetwork"), self.network_obj_params): + self.msg = "Network Functions Config is not applied to the Cisco Catalyst Center" + self.status = "failed" + return self + + self.log("Successfully validated the network functions '{0}'." + .format(config.get("network_management_details").get("site_name")), "INFO") + self.result.get("response")[2].get("network").update({"Validation": "Success"}) + + self.msg = "Successfully validated the Global Pool, Reserve Pool \ + and the Network Functions." + self.status = "success" + return self + + def verify_diff_deleted(self, config): + """ + Validating the Cisco Catalyst Center configuration with the playbook details + when state is deleted (delete). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + self.get_have(config) + self.log("Current State (have): {0}".format(self.have), "INFO") + self.log("Desired State (want): {0}".format(self.want), "INFO") + if config.get("global_pool_details") is not None: + global_pool_exists = self.have.get("globalPool").get("exists") + if global_pool_exists: + self.msg = "Global Pool Config is not applied to the Cisco Catalyst Center" + self.status = "failed" + return self + + self.log("Successfully validated absence of Global Pool '{0}'." + .format(config.get("global_pool_details") + .get("settings").get("ip_pool")[0].get("name")), "INFO") + self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) + + if config.get("reserve_pool_details") is not None: + reserve_pool_exists = self.have.get("reservePool").get("exists") + if reserve_pool_exists: + self.msg = "Reserved Pool Config is not applied to the Catalyst Center" + self.status = "failed" + return self + + self.log("Successfully validated the absence of Reserve Pool '{0}'." + .format(config.get("reserve_pool_details").get("name")), "INFO") + self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) + + self.msg = "Successfully validated the absence of Global Pool/Reserve Pool" + self.status = "success" + return self + + def reset_values(self): + """ + Reset all neccessary attributes to default values + + Parameters: + None + + Returns: + None + """ + + self.have.clear() + self.want.clear() + return + + +def main(): + """main entry point for module execution""" + + # Define the specification for module arguments + element_spec = { + "dnac_host": {"type": 'str', "required": True}, + "dnac_port": {"type": 'str', "default": '443'}, + "dnac_username": {"type": 'str', "default": 'admin', "aliases": ['user']}, + "dnac_password": {"type": 'str', "no_log": True}, + "dnac_verify": {"type": 'bool', "default": 'True'}, + "dnac_version": {"type": 'str', "default": '2.2.3.3'}, + "dnac_debug": {"type": 'bool', "default": False}, + "dnac_log": {"type": 'bool', "default": False}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + "config_verify": {"type": 'bool', "default": False}, + "config": {"type": 'list', "required": True, "elements": 'dict'}, + "state": {"default": 'merged', "choices": ['merged', 'deleted']}, + "validate_response_schema": {"type": 'bool', "default": True}, + } + + # Create an AnsibleModule object with argument specifications + module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) + ccc_network = NetworkSettings(module) + state = ccc_network.params.get("state") + config_verify = ccc_network.params.get("config_verify") + if state not in ccc_network.supported_states: + ccc_network.status = "invalid" + ccc_network.msg = "State {0} is invalid".format(state) + ccc_network.check_return_status() + + ccc_network.validate_input().check_return_status() + + for config in ccc_network.config: + ccc_network.reset_values() + ccc_network.get_have(config).check_return_status() + if state != "deleted": + ccc_network.get_want(config).check_return_status() + ccc_network.get_diff_state_apply[state](config).check_return_status() + if config_verify: + ccc_network.verify_diff_state_apply[state](config).check_return_status() + + module.exit_json(**ccc_network.result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py new file mode 100644 index 0000000000..95ac2ce6e4 --- /dev/null +++ b/plugins/modules/template_workflow_manager.py @@ -0,0 +1,2793 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +"""Ansible module to perform operations on project and templates in Cisco Catalyst Center.""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ['Muthu Rakesh, Madhan Sankaranarayanan'] + +DOCUMENTATION = r""" +--- +module: template_workflow_manager +short_description: Resource module for Template functions +description: +- Manage operations create, update and delete of the resource Configuration Template. +- API to create a template by project name and template name. +- API to update a template by template name and project name. +- API to delete a template by template name and project name. +- API to export the projects for given projectNames. +- API to export the templates for given templateIds. +- API to manage operation create of the resource Configuration Template Import Project. +- API to manage operation create of the resource Configuration Template Import Template. +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Muthu Rakesh (@MUTHU-RAKESH-27) + Madhan Sankaranarayanan (@madhansansel) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center after applying the playbook config. + type: bool + default: False + state: + description: The state of Cisco Catalyst Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of templates being managed. + type: list + elements: dict + required: true + suboptions: + configuration_templates: + description: Create/Update/Delete template. + type: dict + suboptions: + author: + description: Author of template. + type: str + composite: + description: Is it composite template. + type: bool + containing_templates: + description: Configuration Template Create's containingTemplates. + suboptions: + composite: + description: Is it composite template. + type: bool + description: + description: Description of template. + type: str + device_types: + description: deviceTypes on which templates would be applied. + type: list + elements: dict + suboptions: + product_family: + description: Device family. + type: str + product_series: + description: Device series. + type: str + product_type: + description: Device type. + type: str + id: + description: UUID of template. + type: str + language: + description: Template language + choices: + - JINJA + - VELOCITY + type: str + name: + description: Name of template. + type: str + project_name: + description: Name of the project under which templates are managed. + type: str + project_description: + description: Description of the project created. + type: str + rollback_template_params: + description: Params required for template rollback. + type: list + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + custom_order: + description: CustomOrder of template param. + type: int + data_type: + description: Datatype of template param. + type: str + default_value: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + display_name: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instruction_text: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + not_param: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + param_array: + description: Is it an array. + type: bool + parameter_name: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Create's range. + type: list + elements: dict + suboptions: + id: + description: UUID of range. + type: str + max_value: + description: Max value of range. + type: int + min_value: + description: Min value of range. + type: int + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Create's selection. + suboptions: + default_selected_values: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selection_type: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selection_values: + description: Selection values. + type: dict + type: dict + tags: + description: Configuration Template Create's tags. + suboptions: + id: + description: UUID of tag. + type: str + name: + description: Name of tag. + type: str + type: list + elements: dict + template_content: + description: Template content. + type: str + template_params: + description: Configuration Template Create's templateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + custom_order: + description: CustomOrder of template param. + type: int + data_type: + description: Datatype of template param. + type: str + default_value: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + display_name: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instruction_text: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + not_param: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + param_array: + description: Is it an array. + type: bool + parameter_name: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Create's range. + suboptions: + id: + description: UUID of range. + type: str + max_value: + description: Max value of range. + type: int + min_value: + description: Min value of range. + type: int + type: list + elements: dict + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Create's selection. + suboptions: + default_selected_values: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selection_type: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selection_values: + description: Selection values. + type: dict + type: dict + type: list + version: + description: Current version of template. + type: str + type: list + elements: dict + create_time: + description: Create time of template. + type: int + custom_params_order: + description: Custom Params Order. + type: bool + template_description: + description: Description of template. + type: str + device_types: + description: Configuration Template Create's deviceTypes. This field is mandatory to create a new template. + suboptions: + product_family: + description: Device family. + type: str + product_series: + description: Device series. + type: str + product_type: + description: Device type. + type: str + type: list + elements: dict + failure_policy: + description: Define failure policy if template provisioning fails. + type: str + id: + description: UUID of template. + type: str + language: + description: Template language + choices: + - JINJA + - VELOCITY + type: str + last_update_time: + description: Update time of template. + type: int + latest_version_time: + description: Latest versioned template time. + type: int + template_name: + description: Name of template. This field is mandatory to create a new template. + type: str + parent_template_id: + description: Parent templateID. + type: str + project_id: + description: Project UUID. + type: str + project_name: + description: Project name. + type: str + project_description: + description: Project Description. + type: str + rollback_template_content: + description: Rollback template content. + type: str + rollback_template_params: + description: Configuration Template Create's rollbackTemplateParams. + suboptions: + binding: + description: Bind to source. + type: str + custom_order: + description: CustomOrder of template param. + type: int + data_type: + description: Datatype of template param. + type: str + default_value: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + display_name: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instruction_text: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + not_param: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + param_array: + description: Is it an array. + type: bool + parameter_name: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Create's range. + suboptions: + id: + description: UUID of range. + type: str + max_value: + description: Max value of range. + type: int + min_value: + description: Min value of range. + type: int + type: list + elements: dict + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Create's selection. + suboptions: + default_selected_values: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selection_type: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selection_values: + description: Selection values. + type: dict + type: dict + type: list + elements: dict + software_type: + description: Applicable device software type. This field is mandatory to create a new template. + type: str + software_variant: + description: Applicable device software variant. + type: str + software_version: + description: Applicable device software version. + type: str + template_tag: + description: Configuration Template Create's tags. + suboptions: + id: + description: UUID of tag. + type: str + name: + description: Name of tag. + type: str + type: list + elements: dict + template_content: + description: Template content. + type: str + template_params: + description: Configuration Template Create's templateParams. + suboptions: + binding: + description: Bind to source. + type: str + custom_order: + description: CustomOrder of template param. + type: int + data_type: + description: Datatype of template param. + type: str + default_value: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + display_name: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instruction_text: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + not_param: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + param_array: + description: Is it an array. + type: bool + parameter_name: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Create's range. + suboptions: + id: + description: UUID of range. + type: str + max_value: + description: Max value of range. + type: int + min_value: + description: Min value of range. + type: int + type: list + elements: dict + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Create's selection. + suboptions: + default_selected_values: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selection_type: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selection_values: + description: Selection values. + type: dict + type: dict + type: list + elements: dict + validation_errors: + description: Configuration Template Create's validationErrors. + suboptions: + rollback_template_errors: + description: Validation or design conflicts errors of rollback template. + elements: dict + type: list + template_errors: + description: Validation or design conflicts errors. + elements: dict + type: list + template_id: + description: UUID of template. + type: str + template_version: + description: Current version of template. + type: str + type: dict + version: + description: Current version of template. + type: str + version_description: + description: Template version comments. + type: str + export: + description: Export the project/template details. + type: dict + suboptions: + project: + description: Export the project. + type: list + elements: str + template: + description: Export the template. + type: list + elements: dict + suboptions: + project_name: + description: Name of the project under the template available. + type: str + template_name: + description: Name of the template which we need to export + type: str + import: + description: Import the project/template details. + type: dict + suboptions: + project: + description: Import the project details. + type: dict + suboptions: + do_version: + description: DoVersion query parameter. If this flag is true, creates a new + version of the template with the imported contents, if the templates already + exists. " If false and if template already exists, then operation + fails with 'Template already exists' error. + type: bool + template: + description: Import the template details. + type: dict + suboptions: + do_version: + description: DoVersion query parameter. If this flag is true, creates a new + version of the template with the imported contents, if the templates already + exists. " If false and if template already exists, then operation + fails with 'Template already exists' error. + type: bool + payload: + description: Configuration Template Import Template's payload. + elements: dict + suboptions: + author: + description: Author of template. + type: str + composite: + description: Is it composite template. + type: bool + containing_templates: + description: Configuration Template Import Template's containingTemplates. + elements: dict + suboptions: + composite: + description: Is it composite template. + type: bool + description: + description: Description of template. + type: str + device_types: + description: Configuration Template Import Template's deviceTypes. + elements: dict + suboptions: + product_family: + description: Device family. + type: str + product_series: + description: Device series. + type: str + product_type: + description: Device type. + type: str + type: list + id: + description: UUID of template. + type: str + language: + description: Template language (JINJA or VELOCITY). + type: str + name: + description: Name of template. + type: str + project_name: + description: Project name. + type: str + rollback_template_params: + description: Configuration Template Import Template's rollbackTemplateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + custom_order: + description: CustomOrder of template param. + type: int + data_type: + description: Datatype of template param. + type: str + default_value: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + display_name: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instruction_text: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + not_param: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + param_array: + description: Is it an array. + type: bool + parameter_name: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Import Template's range. + elements: dict + suboptions: + id: + description: UUID of range. + type: str + max_value: + description: Max value of range. + type: int + min_value: + description: Min value of range. + type: int + type: list + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Import Template's selection. + suboptions: + default_selected_values: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selection_type: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selection_values: + description: Selection values. + type: dict + type: dict + type: list + tags: + description: Configuration Template Import Template's tags. + elements: dict + suboptions: + id: + description: UUID of tag. + type: str + name: + description: Name of tag. + type: str + type: list + template_content: + description: Template content. + type: str + template_params: + description: Configuration Template Import Template's templateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + custom_order: + description: CustomOrder of template param. + type: int + data_type: + description: Datatype of template param. + type: str + default_value: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + display_name: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instruction_text: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + not_param: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + param_array: + description: Is it an array. + type: bool + parameter_name: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Import Template's range. + elements: dict + suboptions: + id: + description: UUID of range. + type: str + max_value: + description: Max value of range. + type: int + min_value: + description: Min value of range. + type: int + type: list + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Import Template's selection. + suboptions: + default_selected_values: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selection_type: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selection_values: + description: Selection values. + type: dict + type: dict + type: list + version: + description: Current version of template. + type: str + type: list + create_time: + description: Create time of template. + type: int + custom_params_order: + description: Custom Params Order. + type: bool + description: + description: Description of template. + type: str + device_types: + description: Configuration Template Import Template's deviceTypes. + elements: dict + suboptions: + product_family: + description: Device family. + type: str + product_series: + description: Device series. + type: str + product_type: + description: Device type. + type: str + type: list + failure_policy: + description: Define failure policy if template provisioning fails. + type: str + id: + description: UUID of template. + type: str + language: + description: Template language (JINJA or VELOCITY). + type: str + last_update_time: + description: Update time of template. + type: int + latest_version_time: + description: Latest versioned template time. + type: int + name: + description: Name of template. + type: str + parent_template_id: + description: Parent templateID. + type: str + project_id: + description: Project UUID. + type: str + project_name: + description: Project name. + type: str + rollback_template_content: + description: Rollback template content. + type: str + rollback_template_params: + description: Configuration Template Import Template's rollbackTemplateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + custom_order: + description: CustomOrder of template param. + type: int + data_type: + description: Datatype of template param. + type: str + default_value: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + display_name: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instruction_text: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + not_param: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + param_array: + description: Is it an array. + type: bool + parameter_name: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Import Template's range. + elements: dict + suboptions: + id: + description: UUID of range. + type: str + max_value: + description: Max value of range. + type: int + min_value: + description: Min value of range. + type: int + type: list + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Import Template's selection. + suboptions: + default_selected_values: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selection_type: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selection_values: + description: Selection values. + type: dict + type: dict + type: list + software_type: + description: Applicable device software type. + type: str + software_variant: + description: Applicable device software variant. + type: str + software_version: + description: Applicable device software version. + type: str + tags: + description: Configuration Template Import Template's tags. + elements: dict + suboptions: + id: + description: UUID of tag. + type: str + name: + description: Name of tag. + type: str + type: list + template_content: + description: Template content. + type: str + template_params: + description: Configuration Template Import Template's templateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + custom_order: + description: CustomOrder of template param. + type: int + data_type: + description: Datatype of template param. + type: str + default_value: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + display_name: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instruction_text: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + not_param: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + param_array: + description: Is it an array. + type: bool + parameter_name: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Import Template's range. + elements: dict + suboptions: + id: + description: UUID of range. + type: str + max_value: + description: Max value of range. + type: int + min_value: + description: Min value of range. + type: int + type: list + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Import Template's selection. + suboptions: + default_selected_values: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selection_type: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selection_values: + description: Selection values. + type: dict + type: dict + type: list + validation_errors: + description: Configuration Template Import Template's validationErrors. + suboptions: + rollback_template_errors: + description: Validation or design conflicts errors of rollback template. + type: dict + template_errors: + description: Validation or design conflicts errors. + type: dict + template_id: + description: UUID of template. + type: str + template_version: + description: Current version of template. + type: str + type: dict + version: + description: Current version of template. + type: str + type: list + project_name: + description: ProjectName path parameter. Project name to create template under the + project. + type: str + +requirements: +- dnacentersdk == 2.4.5 +- python >= 3.5 +notes: + - SDK Method used are + configuration_templates.ConfigurationTemplates.create_template, + configuration_templates.ConfigurationTemplates.deletes_the_template, + configuration_templates.ConfigurationTemplates.update_template, + configuration_templates.ConfigurationTemplates.export_projects, + configuration_templates.ConfigurationTemplates.export_templates, + configuration_templates.ConfigurationTemplates.imports_the_projects_provided, + configuration_templates.ConfigurationTemplates.imports_the_templates_provided, + + - Paths used are + post /dna/intent/api/v1/template-programmer/project/{projectId}/template, + delete /dna/intent/api/v1/template-programmer/template/{templateId}, + put /dna/intent/api/v1/template-programmer/template, + post /dna/intent/api/v1/template-programmer/project/name/exportprojects, + post /dna/intent/api/v1/template-programmer/template/exporttemplates, + post /dna/intent/api/v1/template-programmer/project/importprojects, + post /dna/intent/api/v1/template-programmer/project/name/{projectName}/template/importtemplates, + +""" + +EXAMPLES = r""" +- name: Create a new template, export and import the project and template. + cisco.dnac.template_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: merged + config_verify: True + config: + - configuration_templates: + author: string + composite: true + create_time: 0 + custom_params_order: true + description: string + device_types: + - product_family: string + product_series: string + product_type: string + failure_policy: string + id: string + language: string + last_update_time: 0 + latest_version_time: 0 + name: string + parent_template_id: string + project_id: string + project_name: string + project_description: string + rollback_template_content: string + software_type: string + software_variant: string + software_version: string + tags: + - id: string + name: string + template_content: string + validation_errors: + rollback_template_errors: + - {} + template_errors: + - {} + template_id: string + template_version: string + version: string + export: + project: + - string + template: + - project_name : string + template_name: string + import: + project: + do_version: true + export: + do_version: true + payload: + - author: string + composite: true + containing_templates: + - composite: true + description: string + device_types: + - product_family: string + product_series: string + product_type: string + id: string + language: string + name: string + project_name: string + rollback_template_params: + - binding: string + custom_order: 0 + data_type: string + default_value: string + description: string + display_name: string + group: string + id: string + instruction_text: string + key: string + not_param: true + order: 0 + param_array: true + parameter_name: string + provider: string + range: + - id: string + project_name: string + + +""" + +RETURN = r""" +# Case_1: Successful creation/updation/deletion of template/project +response_1: + description: A dictionary with versioning details of the template as returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": { + "endTime": 0, + "version": 0, + "data": String, + "startTime": 0, + "username": String, + "progress": String, + "serviceType": String, "rootId": String, + "isError": bool, + "instanceTenantId": String, + "id": String + "version": 0 + }, + "msg": String + } + +# Case_2: Error while deleting a template or when given project is not found +response_2: + description: A list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: list + sample: > + { + "response": [], + "msg": String + } + +# Case_3: Given template already exists and requires no update +response_3: + description: A dictionary with the exisiting template deatails as returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": {}, + "msg": String + } + +# Case_4: Given template list that needs to be exported +response_4: + description: Details of the templates in the list as returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": {}, + "msg": String + } + +# Case_5: Given project list that needs to be exported +response_5: + description: Details of the projects in the list as returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": {}, + "msg": String + } + +""" + +import copy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, + get_dict_result, + dnac_compare_equality, +) + + +class Template(DnacBase): + """Class containing member attributes for template intent module""" + + def __init__(self, module): + super().__init__(module) + self.have_project = {} + self.have_template = {} + self.supported_states = ["merged", "deleted"] + self.accepted_languages = ["JINJA", "VELOCITY"] + self.export_template = [] + self.result['response'].append({}) + + def validate_input(self): + """ + Validate the fields provided in the playbook. + Checks the configuration provided in the playbook against a predefined specification + to ensure it adheres to the expected structure and data types. + Parameters: + self: The instance of the class containing the 'config' attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of 'config' parameter. + Example: + To use this method, create an instance of the class and call 'validate_input' on it. + If the validation succeeds, 'self.status' will be 'success' and 'self.validated_config' + will contain the validated configuration. If it fails, 'self.status' will be 'failed', + 'self.msg' will describe the validation issues. + + """ + + if not self.config: + self.msg = "config not available in playbook for validattion" + self.status = "success" + return self + + temp_spec = { + "configuration_templates": { + 'type': 'dict', + 'tags': {'type': 'list'}, + 'author': {'type': 'str'}, + 'composite': {'type': 'bool'}, + 'containing_templates': {'type': 'list'}, + 'create_time': {'type': 'int'}, + 'custom_params_order': {'type': 'bool'}, + 'description': {'type': 'str'}, + 'device_types': { + 'type': 'list', + 'elements': 'dict', + 'productFamily': {'type': 'str'}, + 'productSeries': {'type': 'str'}, + 'productType': {'type': 'str'}, + }, + 'failure_policy': {'type': 'str'}, + 'id': {'type': 'str'}, + 'language': {'type': 'str'}, + 'last_update_time': {'type': 'int'}, + 'latest_version_time': {'type': 'int'}, + 'name': {'type': 'str'}, + 'parent_template_id': {'type': 'str'}, + 'project_id': {'type': 'str'}, + 'project_name': {'type': 'str'}, + 'project_description': {'type': 'str'}, + 'rollback_template_content': {'type': 'str'}, + 'rollback_template_params': {'type': 'list'}, + 'software_type': {'type': 'str'}, + 'software_variant': {'type': 'str'}, + 'software_version': {'type': 'str'}, + 'template_content': {'type': 'str'}, + 'template_params': {'type': 'list'}, + 'template_name': {'type': 'str'}, + 'validation_errors': {'type': 'dict'}, + 'version': {'type': 'str'}, + 'version_description': {'type': 'str'} + }, + 'export': { + 'type': 'dict', + 'project': {'type': 'list', 'elements': 'str'}, + 'template': { + 'type': 'list', + 'elements': 'dict', + 'project_name': {'type': 'str'}, + 'template_name': {'type': 'str'} + } + }, + 'import': { + 'type': 'dict', + 'project': { + 'type': 'dict', + 'do_version': {'type': 'str', 'default': 'False'}, + }, + 'template': { + 'type': 'dict', + 'do_version': {'type': 'str', 'default': 'False'}, + 'payload': { + 'type': 'list', + 'elements': 'dict', + 'tags': {'type': 'list'}, + 'author': {'type': 'str'}, + 'composite': {'type': 'bool'}, + 'containing_templates': {'type': 'list'}, + 'create_time': {'type': 'int'}, + 'custom_params_order': {'type': 'bool'}, + 'description': {'type': 'str'}, + 'device_types': { + 'type': 'list', + 'elements': 'dict', + 'product_family': {'type': 'str'}, + 'product_series': {'type': 'str'}, + 'product_type': {'type': 'str'}, + }, + 'failure_policy': {'type': 'str'}, + 'id': {'type': 'str'}, + 'language': {'type': 'str'}, + 'last_update_time': {'type': 'int'}, + 'latest_version_time': {'type': 'int'}, + 'name': {'type': 'str'}, + 'parent_template_id': {'type': 'str'}, + 'project_id': {'type': 'str'}, + 'project_name': {'type': 'str'}, + 'project_description': {'type': 'str'}, + 'rollback_template_content': {'type': 'str'}, + 'rollback_template_params': {'type': 'list'}, + 'software_type': {'type': 'str'}, + 'software_variant': {'type': 'str'}, + 'software_version': {'type': 'str'}, + 'template_content': {'type': 'str'}, + 'template_params': {'type': 'list'}, + 'template_name': {'type': 'str'}, + 'validation_errors': {'type': 'dict'}, + 'version': {'type': 'str'} + } + } + } + } + # Validate template params + self.config = self.camel_to_snake_case(self.config) + valid_temp, invalid_params = validate_list_of_dicts( + self.config, temp_spec + ) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params)) + self.status = "failed" + return self + + self.validated_config = valid_temp + self.log("Successfully validated playbook config params: {0}".format(valid_temp), "INFO") + self.msg = "Successfully validated input" + self.status = "success" + return self + + def get_project_params(self, params): + """ + Store project parameters from the playbook for template processing in Cisco Catalyst Center. + + Parameters: + params (dict) - Playbook details containing Project information. + + Returns: + project_params (dict) - Organized Project parameters. + """ + + project_params = {"name": params.get("project_name"), + "description": params.get("project_description") + } + return project_params + + def get_tags(self, _tags): + """ + Store tags from the playbook for template processing in Cisco Catalyst Center. + Check using check_return_status() + + Parameters: + tags (dict) - Tags details containing Template information. + + Returns: + tags (dict) - Organized tags parameters. + """ + + if _tags is None: + return None + + tags = [] + i = 0 + for item in _tags: + tags.append({}) + id = item.get("id") + if id is not None: + tags[i].update({"id": id}) + + name = item.get("name") + if name is not None: + tags[i].update({"name": name}) + else: + self.msg = "name is mandatory in tags in location " + str(i) + self.status = "failed" + return self.check_return_status() + + return tags + + def get_device_types(self, device_types): + """ + Store device types parameters from the playbook for template processing in Cisco Catalyst Center. + Check using check_return_status() + + Parameters: + device_types (dict) - Device types details containing Template information. + + Returns: + deviceTypes (dict) - Organized device types parameters. + """ + + if device_types is None: + return None + + deviceTypes = [] + i = 0 + for item in device_types: + deviceTypes.append({}) + product_family = item.get("product_family") + if product_family is not None: + deviceTypes[i].update({"productFamily": product_family}) + else: + self.msg = "product_family is mandatory for deviceTypes" + self.status = "failed" + return self.check_return_status() + + product_series = item.get("product_series") + if product_series is not None: + deviceTypes[i].update({"productSeries": product_series}) + product_type = item.get("product_type") + if product_type is not None: + deviceTypes[i].update({"productType": product_type}) + i = i + 1 + + return deviceTypes + + def get_validation_errors(self, validation_errors): + """ + Store template parameters from the playbook for template processing in Cisco Catalyst Center. + + Parameters: + validation_errors (dict) - Playbook details containing validation errors information. + + Returns: + validationErrors (dict) - Organized validation errors parameters. + """ + + if validation_errors is None: + return None + + validationErrors = {} + rollback_template_errors = validation_errors.get("rollback_template_errors") + if rollback_template_errors is not None: + validationErrors.update({ + "rollbackTemplateErrors": rollback_template_errors + }) + + template_errors = validation_errors.get("template_errors") + if template_errors is not None: + validationErrors.update({ + "templateErrors": template_errors + }) + + template_id = validation_errors.get("template_id") + if template_id is not None: + validationErrors.update({ + "templateId": template_id + }) + + template_version = validation_errors.get("template_version") + if template_version is not None: + validationErrors.update({ + "templateVersion": template_version + }) + + return validationErrors + + def get_template_info(self, template_params): + """ + Store template params from the playbook for template processing in Cisco Catalyst Center. + Check using check_return_status() + + Parameters: + template_params (dict) - Playbook details containing template params information. + + Returns: + templateParams (dict) - Organized template params parameters. + """ + + if template_params is None: + return None + + templateParams = [] + i = 0 + self.log("Template params details: {0}".format(template_params), "DEBUG") + for item in template_params: + self.log("Template params items: {0}".format(item), "DEBUG") + templateParams.append({}) + binding = item.get("binding") + if binding is not None: + templateParams[i].update({"binding": binding}) + + custom_order = item.get("custom_order") + if custom_order is not None: + templateParams[i].update({"customOrder": custom_order}) + + default_value = item.get("default_value") + if default_value is not None: + templateParams[i].update({"defaultValue": default_value}) + + description = item.get("description") + if description is not None: + templateParams[i].update({"description": description}) + + display_name = item.get("display_name") + if display_name is not None: + templateParams[i].update({"displayName": display_name}) + + group = item.get("group") + if group is not None: + templateParams[i].update({"group": group}) + + id = item.get("id") + if id is not None: + templateParams[i].update({"id": id}) + + instruction_text = item.get("instruction_text") + if instruction_text is not None: + templateParams[i].update({"instructionText": instruction_text}) + + key = item.get("key") + if key is not None: + templateParams[i].update({"key": key}) + + not_param = item.get("not_param") + if not_param is not None: + templateParams[i].update({"notParam": not_param}) + + order = item.get("order") + if order is not None: + templateParams[i].update({"order": order}) + + param_array = item.get("param_array") + if param_array is not None: + templateParams[i].update({"paramArray": param_array}) + + provider = item.get("provider") + if provider is not None: + templateParams[i].update({"provider": provider}) + + parameter_name = item.get("parameter_name") + if parameter_name is not None: + templateParams[i].update({"parameterName": parameter_name}) + else: + self.msg = "parameter_name is mandatory for the template_params." + self.status = "failed" + return self.check_return_status() + + data_type = item.get("data_type") + datatypes = ["STRING", "INTEGER", "IPADDRESS", "MACADDRESS", "SECTIONDIVIDER"] + if data_type is not None: + templateParams[i].update({"dataType": data_type}) + else: + self.msg = "dataType is mandatory for the template_params." + self.status = "failed" + return self.check_return_status() + if data_type not in datatypes: + self.msg = "data_type under template_params should be in " + str(datatypes) + self.status = "failed" + return self.check_return_status() + + required = item.get("required") + if required is not None: + templateParams[i].update({"required": required}) + + range = item.get("range") + self.log("Template params range list: {0}".format(range), "DEBUG") + if range is not None: + templateParams[i].update({"range": []}) + _range = templateParams[i].get("range") + self.log("Template params range: {0}".format(_range), "DEBUG") + j = 0 + for value in range: + _range.append({}) + id = value.get("id") + if id is not None: + _range[j].update({"id": id}) + max_value = value.get("max_value") + if max_value is not None: + _range[j].update({"maxValue": max_value}) + else: + self.msg = "max_value is mandatory for range under template_params" + self.status = "failed" + return self.check_return_status() + min_value = value.get("min_value") + if min_value is not None: + _range[j].update({"maxValue": min_value}) + else: + self.msg = "min_value is mandatory for range under template_params" + self.status = "failed" + return self.check_return_status() + j = j + 1 + + self.log("Template params details: {0}".format(templateParams), "DEBUG") + selection = item.get("selection") + self.log("Template params selection: {0}".format(selection), "DEBUG") + if selection is not None: + templateParams[i].update({"selection": {}}) + _selection = templateParams[i].get("selection") + id = selection.get("id") + if id is not None: + _selection.update({"id": id}) + default_selected_values = selection.get("default_selected_values") + if default_selected_values is not None: + _selection.update({"defaultSelectedValues": default_selected_values}) + selection_values = selection.get("selection_values") + if selection_values is not None: + _selection.update({"selectionValues": selection_values}) + selection_type = selection.get("selection_type") + if selection_type is not None: + _selection.update({"selectionType": selection_type}) + i = i + 1 + + return templateParams + + def get_containing_templates(self, containing_templates): + """ + Store tags from the playbook for template processing in Cisco Catalyst Center. + Check using check_return_status() + + Parameters: + containing_templates (dict) - Containing tempaltes details + containing Template information. + + Returns: + containingTemplates (dict) - Organized containing templates parameters. + """ + + if containing_templates is None: + return None + + containingTemplates = [] + i = 0 + for item in containing_templates: + containingTemplates.append({}) + _tags = item.get("tags") + if _tags is not None: + containingTemplates[i].update({"tags": self.get_tags(_tags)}) + + composite = item.get("composite") + if composite is not None: + containingTemplates[i].update({"composite": composite}) + + description = item.get("description") + if description is not None: + containingTemplates[i].update({"description": description}) + + device_types = item.get("device_types") + if device_types is not None: + containingTemplates[i].update({ + "deviceTypes": self.get_device_types(device_types) + }) + + id = item.get("id") + if id is not None: + containingTemplates[i].update({"id": id}) + + language = item.get("language") + if language is not None: + containingTemplates[i].update({"language": language}) + else: + self.msg = "language is mandatory under containing templates" + self.status = "failed" + return self.check_return_status() + + name = item.get("name") + name_list = ["JINJA", "VELOCITY"] + if name is not None: + containingTemplates[i].update({"name": name}) + else: + self.msg = "name is mandatory under containing templates" + self.status = "failed" + return self.check_return_status() + if name not in name_list: + self.msg = "name under containing templates should be in " + str(name_list) + self.status = "failed" + return self.check_return_status() + + project_name = item.get("project_name") + if project_name is not None: + containingTemplates[i].update({"projectName": project_name}) + else: + self.msg = "project_name is mandatory under containing templates" + self.status = "failed" + return self.check_return_status() + + rollback_template_params = item.get("rollback_template_params") + if rollback_template_params is not None: + containingTemplates[i].update({ + "rollbackTemplateParams": self.get_template_info(rollback_template_params) + }) + + template_content = item.get("template_content") + if template_content is not None: + containingTemplates[i].update({"templateContent": template_content}) + + template_params = item.get("template_params") + if template_params is not None: + containingTemplates[i].update({ + "templateParams": self.get_template_info(template_params) + }) + + version = item.get("version") + if version is not None: + containingTemplates[i].update({"version": version}) + + return containingTemplates + + def get_template_params(self, params): + """ + Store template parameters from the playbook for template processing in Cisco Catalyst Center. + + Parameters: + params (dict) - Playbook details containing Template information. + + Returns: + temp_params (dict) - Organized template parameters. + """ + + self.log("Template params playbook details: {0}".format(params), "DEBUG") + temp_params = { + "tags": self.get_tags(params.get("template_tag")), + "author": params.get("author"), + "composite": params.get("composite"), + "containingTemplates": + self.get_containing_templates(params.get("containing_templates")), + "createTime": params.get("create_time"), + "customParamsOrder": params.get("custom_params_order"), + "description": params.get("template_description"), + "deviceTypes": + self.get_device_types(params.get("device_types")), + "failurePolicy": params.get("failure_policy"), + "id": params.get("id"), + "language": params.get("language").upper(), + "lastUpdateTime": params.get("last_update_time"), + "latestVersionTime": params.get("latest_version_time"), + "name": params.get("template_name"), + "parentTemplateId": params.get("parent_template_id"), + "projectId": params.get("project_id"), + "projectName": params.get("project_name"), + "rollbackTemplateContent": params.get("rollback_template_content"), + "rollbackTemplateParams": + self.get_template_info(params.get("rollback_template_params")), + "softwareType": params.get("software_type"), + "softwareVariant": params.get("software_variant"), + "softwareVersion": params.get("software_version"), + "templateContent": params.get("template_content"), + "templateParams": + self.get_template_info(params.get("template_params")), + "validationErrors": + self.get_validation_errors(params.get("validation_errors")), + "version": params.get("version"), + "project_id": params.get("project_id") + } + self.log("Formatted template params details: {0}".format(temp_params), "DEBUG") + copy_temp_params = copy.deepcopy(temp_params) + for item in copy_temp_params: + if temp_params[item] is None: + del temp_params[item] + self.log("Formatted template params details: {0}".format(temp_params), "DEBUG") + return temp_params + + def get_template(self, config): + """ + Get the template needed for updation or creation. + + Parameters: + config (dict) - Playbook details containing Template information. + + Returns: + result (dict) - Template details for the given template ID. + """ + + result = None + items = self.dnac_apply['exec']( + family="configuration_templates", + function="get_template_details", + params={"template_id": config.get("templateId")} + ) + if items: + result = items + + self.log("Received API response from 'get_template_details': {0}".format(items), "DEBUG") + self.result['response'] = items + return result + + def get_have_project(self, config): + """ + Get the current project related information from Cisco Catalyst Center. + + Parameters: + config (dict) - Playbook details containing Project information. + + Returns: + template_available (list) - Current project information. + """ + + have_project = {} + given_projectName = config.get("configuration_templates").get("project_name") + template_available = None + + # Check if project exists. + project_details = self.get_project_details(given_projectName) + # Cisco Catalyst Center returns project details even if the substring matches. + # Hence check the projectName retrieved from Cisco Catalyst Center. + if not (project_details and isinstance(project_details, list)): + self.log("Project: {0} not found, need to create new project in Cisco Catalyst Center" + .format(given_projectName), "INFO") + return None + + fetched_projectName = project_details[0].get('name') + if fetched_projectName != given_projectName: + self.log("Project {0} provided is not exact match in Cisco Catalyst Center DB" + .format(given_projectName), "INFO") + return None + + template_available = project_details[0].get('templates') + have_project["project_found"] = True + have_project["id"] = project_details[0].get("id") + have_project["isDeletable"] = project_details[0].get("isDeletable") + + self.have_project = have_project + return template_available + + def get_have_template(self, config, template_available): + """ + Get the current template related information from Cisco Catalyst Center. + + Parameters: + config (dict) - Playbook details containing Template information. + template_available (list) - Current project information. + + Returns: + self + """ + + projectName = config.get("configuration_templates").get("project_name") + templateName = config.get("configuration_templates").get("template_name") + template = None + have_template = {} + + have_template["isCommitPending"] = False + have_template["template_found"] = False + + template_details = get_dict_result(template_available, + "name", + templateName) + # Check if specified template in playbook is available + if not template_details: + self.log("Template {0} not found in project {1}" + .format(templateName, projectName), "INFO") + self.msg = "Template : {0} missing, new template to be created".format(templateName) + self.status = "success" + return self + + config["templateId"] = template_details.get("id") + have_template["id"] = template_details.get("id") + # Get available templates which are committed under the project + template_list = self.dnac_apply['exec']( + family="configuration_templates", + function="gets_the_templates_available", + params={"projectNames": config.get("projectName")}, + ) + have_template["isCommitPending"] = True + # This check will fail if specified template is there not committed in Cisco Catalyst Center + if template_list and isinstance(template_list, list): + template_info = get_dict_result(template_list, + "name", + templateName) + if template_info: + template = self.get_template(config) + have_template["template"] = template + have_template["isCommitPending"] = False + have_template["template_found"] = template is not None \ + and isinstance(template, dict) + self.log("Template {0} is found and template " + "details are :{1}".format(templateName, str(template)), "INFO") + + # There are committed templates in the project but the + # one specified in the playbook may not be committed + self.log("Commit pending for template name {0}" + " is {1}".format(templateName, have_template.get('isCommitPending')), "INFO") + + self.have_template = have_template + self.msg = "Successfully collected all template parameters from Cisco Catalyst Center for comparison" + self.status = "success" + return self + + def get_have(self, config): + """ + Get the current project and template details from Cisco Catalyst Center. + + Parameters: + config (dict) - Playbook details containing Project/Template information. + + Returns: + self + """ + configuration_templates = config.get("configuration_templates") + if configuration_templates: + if not configuration_templates.get("project_name"): + self.msg = "Mandatory Parameter project_name not available" + self.status = "failed" + return self + template_available = self.get_have_project(config) + if template_available: + self.get_have_template(config, template_available) + + self.msg = "Successfully collected all project and template \ + parameters from Cisco Catalyst Center for comparison" + self.status = "success" + return self + + def get_project_details(self, projectName): + """ + Get the details of specific project name provided. + + Parameters: + projectName (str) - Project Name + + Returns: + items (dict) - Project details with given project name. + """ + + items = self.dnac_apply['exec']( + family="configuration_templates", + function='get_projects', + op_modifies=True, + params={"name": projectName}, + ) + return items + + def get_want(self, config): + """ + Get all the template and project related information from playbook + that is needed to be created in Cisco Catalyst Center. + + Parameters: + config (dict) - Playbook details. + + Returns: + self + """ + + want = {} + configuration_templates = config.get("configuration_templates") + self.log("Playbook details: {0}".format(config), "INFO") + if configuration_templates: + template_params = self.get_template_params(configuration_templates) + project_params = self.get_project_params(configuration_templates) + version_comments = configuration_templates.get("version_description") + + if self.params.get("state") == "merged": + self.update_mandatory_parameters(template_params) + + want["template_params"] = template_params + want["project_params"] = project_params + want["comments"] = version_comments + + self.want = want + self.msg = "Successfully collected all parameters from playbook " + \ + "for comparison" + self.status = "success" + return self + + def create_project_or_template(self, is_create_project=False): + """ + Call Cisco Catalyst Center API to create project or template based on the input provided. + + Parameters: + is_create_project (bool) - Default value is False. + + Returns: + creation_id (str) - Project Id. + created (str) - True if Project created, else False. + """ + + creation_id = None + created = False + self.log("Desired State (want): {0}".format(self.want), "INFO") + template_params = self.want.get("template_params") + project_params = self.want.get("project_params") + + if is_create_project: + params_key = project_params + name = "project: {0}".format(project_params.get('name')) + validation_string = "Successfully created project" + creation_value = "create_project" + else: + params_key = template_params + name = "template: {0}".format(template_params.get('name')) + validation_string = "Successfully created template" + creation_value = "create_template" + + response = self.dnac_apply['exec']( + family="configuration_templates", + function=creation_value, + op_modifies=True, + params=params_key, + ) + if not isinstance(response, dict): + self.log("Response of '{0}' is not in dictionary format." + .format(creation_value), "CRITICAL") + return creation_id, created + + task_id = response.get("response").get("taskId") + if not task_id: + self.log("Task id {0} not found for '{1}'.".format(task_id, creation_value), "CRITICAL") + return creation_id, created + + while not created: + task_details = self.get_task_details(task_id) + if not task_details: + self.log("Failed to get task details of '{0}' for taskid: {1}" + .format(creation_value, task_id), "CRITICAL") + return creation_id, created + + self.log("Task details for {0}: {1}".format(creation_value, task_details), "DEBUG") + if task_details.get("isError"): + self.log("Error occurred for '{0}' with taskid: {1}" + .format(creation_value, task_id), "ERROR") + return creation_id, created + + if validation_string not in task_details.get("progress"): + self.log("'{0}' progress set to {1} for taskid: {2}" + .format(creation_value, task_details.get('progress'), task_id), "DEBUG") + continue + + task_details_data = task_details.get("data") + value = self.check_string_dictionary(task_details_data) + if value is None: + creation_id = task_details.get("data") + else: + creation_id = value.get("templateId") + if not creation_id: + self.log("Export data is not found for '{0}' with taskid : {1}" + .format(creation_value, task_id), "DEBUG") + continue + + created = True + if is_create_project: + # ProjectId is required for creating a new template. + # Store it with other template parameters. + template_params["projectId"] = creation_id + template_params["project_id"] = creation_id + + self.log("New {0} created with id {1}".format(name, creation_id), "DEBUG") + return creation_id, created + + def requires_update(self): + """ + Check if the template config given requires update. + + Parameters: + self - Current object. + + Returns: + bool - True if any parameter specified in obj_params differs between + current_obj and requested_obj, indicating that an update is required. + False if all specified parameters are equal. + """ + + if self.have_template.get("isCommitPending"): + self.log("Template '{0}' is in saved state and needs to be updated and committed." + .format(self.have_template.get("template").get("name")), "DEBUG") + return True + + current_obj = self.have_template.get("template") + requested_obj = self.want.get("template_params") + self.log("Current State (have): {0}".format(current_obj), "INFO") + self.log("Desired State (want): {0}".format(requested_obj), "INFO") + obj_params = [ + ("tags", "tags", ""), + ("author", "author", ""), + ("composite", "composite", False), + ("containingTemplates", "containingTemplates", []), + ("createTime", "createTime", ""), + ("customParamsOrder", "customParamsOrder", False), + ("description", "description", ""), + ("deviceTypes", "deviceTypes", []), + ("failurePolicy", "failurePolicy", ""), + ("id", "id", ""), + ("language", "language", "VELOCITY"), + ("lastUpdateTime", "lastUpdateTime", ""), + ("latestVersionTime", "latestVersionTime", ""), + ("name", "name", ""), + ("parentTemplateId", "parentTemplateId", ""), + ("projectId", "projectId", ""), + ("projectName", "projectName", ""), + ("rollbackTemplateContent", "rollbackTemplateContent", ""), + ("rollbackTemplateParams", "rollbackTemplateParams", []), + ("softwareType", "softwareType", ""), + ("softwareVariant", "softwareVariant", ""), + ("softwareVersion", "softwareVersion", ""), + ("templateContent", "templateContent", ""), + ("templateParams", "templateParams", []), + ("validationErrors", "validationErrors", {}), + ("version", "version", ""), + ] + + return any(not dnac_compare_equality(current_obj.get(dnac_param, default), + requested_obj.get(ansible_param)) + for (dnac_param, ansible_param, default) in obj_params) + + def update_mandatory_parameters(self, template_params): + """ + Update parameters which are mandatory for creating a template. + + Parameters: + template_params (dict) - Template information. + + Returns: + None + """ + + # Mandate fields required for creating a new template. + # Store it with other template parameters. + template_params["projectId"] = self.have_project.get("id") + template_params["project_id"] = self.have_project.get("id") + # Update language,deviceTypes and softwareType if not provided for existing template. + if not template_params.get("language"): + template_params["language"] = self.have_template.get('template') \ + .get('language') + if not template_params.get("deviceTypes"): + template_params["deviceTypes"] = self.have_template.get('template') \ + .get('deviceTypes') + if not template_params.get("softwareType"): + template_params["softwareType"] = self.have_template.get('template') \ + .get('softwareType') + + def validate_input_merge(self, template_exists): + """ + Validate input after getting all the parameters from Cisco Catalyst Center. + "If mandate like deviceTypes, softwareType and language " + "already present in Cisco Catalyst Center for a template." + "It is not required to be provided in playbook, " + "but if it is new creation error will be thrown to provide these fields. + + Parameters: + template_exists (bool) - True if template exists, else False. + + Returns: + None + """ + + template_params = self.want.get("template_params") + language = template_params.get("language").upper() + if language: + if language not in self.accepted_languages: + self.msg = "Invalid value language {0} ." \ + "Accepted language values are {1}" \ + .format(self.accepted_languages, language) + self.status = "failed" + return self + else: + template_params["language"] = "JINJA" + + if not template_exists: + if not template_params.get("deviceTypes") \ + or not template_params.get("softwareType"): + self.msg = "DeviceTypes and SoftwareType are required arguments to create Templates" + self.status = "failed" + return self + + self.msg = "Input validated for merging" + self.status = "success" + return self + + def get_export_template_values(self, export_values): + """ + Get the export template values from the details provided by the playbook. + + Parameters: + export_values (bool) - All the template available under the project. + + Returns: + self + """ + + template_details = self.dnac._exec( + family="configuration_templates", + function='get_projects_details' + ) + for values in export_values: + project_name = values.get("project_name") + self.log("Project name for export template: {0}".format(project_name), "DEBUG") + template_details = template_details.get("response") + self.log("Template details: {0}".format(template_details), "DEBUG") + all_template_details = get_dict_result(template_details, + "name", + project_name) + self.log("Template details under the project name {0}: {1}" + .format(project_name, all_template_details), "DEBUG") + all_template_details = all_template_details.get("templates") + self.log("Template details under the project name {0}: {1}" + .format(project_name, all_template_details), "DEBUG") + template_name = values.get("template_name") + template_detail = get_dict_result(all_template_details, + "name", + template_name) + self.log("Template details with template name {0}: {1}" + .format(template_name, template_detail), "DEBUG") + if template_detail is None: + self.msg = "Invalid project_name and template_name in export" + self.status = "failed" + return self + self.export_template.append(template_detail.get("id")) + + self.msg = "Successfully collected the export template IDs" + self.status = "success" + return self + + def update_configuration_templates(self, config): + """ + Update/Create templates and projects in CCC with fields provided in Cisco Catalyst Center. + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ + + configuration_templates = config.get("configuration_templates") + if configuration_templates: + is_project_found = self.have_project.get("project_found") + if not is_project_found: + project_id, project_created = \ + self.create_project_or_template(is_create_project=True) + if project_created: + self.log("project created with projectId: {0}".format(project_id), "DEBUG") + else: + self.status = "failed" + self.msg = "Project creation failed" + return self + + is_template_found = self.have_template.get("template_found") + template_params = self.want.get("template_params") + self.log("Desired template details: {0}".format(template_params), "DEBUG") + self.log("Current template details: {0}".format(self.have_template), "DEBUG") + template_id = None + template_updated = False + self.validate_input_merge(is_template_found).check_return_status() + if is_template_found: + if self.requires_update(): + template_id = self.have_template.get("id") + template_params.update({"id": template_id}) + self.log("Current State (have): {0}".format(self.have_template), "INFO") + self.log("Desired State (want): {0}".format(self.want), "INFO") + response = self.dnac_apply['exec']( + family="configuration_templates", + function="update_template", + params=template_params, + op_modifies=True, + ) + template_updated = True + self.log("Updating existing template '{0}'." + .format(self.have_template.get("template").get("name")), "INFO") + else: + # Template does not need update + self.result.update({ + 'response': self.have_template.get("template"), + 'msg': "Template does not need update" + }) + self.status = "exited" + return self + else: + if template_params.get("name"): + template_id, template_updated = self.create_project_or_template() + else: + self.msg = "missing required arguments: template_name" + self.status = "failed" + return self + + if template_updated: + # Template needs to be versioned + version_params = { + "comments": self.want.get("comments"), + "templateId": template_id + } + response = self.dnac_apply['exec']( + family="configuration_templates", + function="version_template", + op_modifies=True, + params=version_params + ) + task_id = response.get("response").get("taskId") + if not task_id: + self.msg = "Task id: {0} not found".format(task_id) + self.status = "failed" + return self + task_details = self.get_task_details(task_id) + self.result['changed'] = True + self.result['msg'] = task_details.get('progress') + self.result['diff'] = config.get("configuration_templates") + self.log("Task details for 'version_template': {0}".format(task_details), "DEBUG") + self.result['response'] = task_details if task_details else response + + if not self.result.get('msg'): + self.msg = "Error while versioning the template" + self.status = "failed" + return self + + def handle_export(self, config): + """ + Export templates and projects in CCC with fields provided in Cisco Catalyst Center. + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ + + export = config.get("export") + if export: + export_project = export.get("project") + self.log("Export project playbook details: {0}" + .format(export_project), "DEBUG") + if export_project: + response = self.dnac._exec( + family="configuration_templates", + function='export_projects', + params={"payload": export_project}, + ) + validation_string = "successfully exported project" + self.check_task_response_status(response, + validation_string, + True).check_return_status() + self.result['response'][0].update({"exportProject": self.msg}) + + export_values = export.get("template") + if export_values: + self.get_export_template_values(export_values).check_return_status() + self.log("Exporting template playbook details: {0}" + .format(self.export_template), "DEBUG") + response = self.dnac._exec( + family="configuration_templates", + function='export_templates', + params={"payload": self.export_template}, + ) + validation_string = "successfully exported template" + self.check_task_response_status(response, + validation_string, + True).check_return_status() + self.result['response'][0].update({"exportTemplate": self.msg}) + + return self + + def handle_import(self, config): + """ + Import templates and projects in CCC with fields provided in Cisco Catalyst Center. + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ + + _import = config.get("import") + if _import: + # _import_project = _import.get("project") + do_version = _import.get("project").get("do_version") + payload = None + if _import.get("project").get("payload"): + payload = _import.get("project").get("payload") + else: + self.msg = "Mandatory parameter payload is not found under import project" + self.status = "failed" + return self + _import_project = { + "doVersion": do_version, + # "payload": "{0}".format(payload) + "payload": payload + } + self.log("Importing project details from the playbook: {0}" + .format(_import_project), "DEBUG") + if _import_project: + response = self.dnac._exec( + family="configuration_templates", + function='imports_the_projects_provided', + params=_import_project, + ) + validation_string = "successfully imported project" + self.check_task_response_status(response, validation_string).check_return_status() + self.result['response'][0].update({"importProject": validation_string}) + + _import_template = _import.get("template") + if _import_template.get("project_name"): + self.msg = "Mandatory paramter project_name is not found under import template" + self.status = "failed" + return self + if _import_template.get("payload"): + self.msg = "Mandatory paramter payload is not found under import template" + self.status = "failed" + return self + + payload = _import_template.get("project_name") + import_template = { + "doVersion": _import_template.get("do_version"), + "projectName": _import_template.get("project_name"), + "payload": self.get_template_params(payload) + } + self.log("Import template details from the playbook: {0}" + .format(_import_template), "DEBUG") + if _import_template: + response = self.dnac._exec( + family="configuration_templates", + function='imports_the_templates_provided', + params=import_template, + ) + validation_string = "successfully imported template" + self.check_task_response_status(response, validation_string).check_return_status() + self.result['response'][0].update({"importTemplate": validation_string}) + + return self + + def get_diff_merged(self, config): + """ + Update/Create templates and projects in CCC with fields provided in Cisco Catalyst Center. + Export the tempaltes and projects. + Import the templates and projects. + Check using check_return_status(). + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ + + self.update_configuration_templates(config) + if self.status == "failed": + return self + + self.handle_export(config) + if self.status == "failed": + return self + + self.handle_import(config) + if self.status == "failed": + return self + + self.msg = "Successfully completed merged state execution" + self.status = "success" + return self + + def delete_project_or_template(self, config, is_delete_project=False): + """ + Call Cisco Catalyst Center API to delete project or template with provided inputs. + + Parameters: + config (dict) - Playbook details containing template information. + is_delete_project (bool) - True if we need to delete project, else False. + + Returns: + self + """ + + if is_delete_project: + params_key = {"project_id": self.have_project.get("id")} + deletion_value = "deletes_the_project" + name = "project: {0}".format(config.get("configuration_templates").get('project_name')) + else: + template_params = self.want.get("template_params") + params_key = {"template_id": self.have_template.get("id")} + deletion_value = "deletes_the_template" + name = "templateName: {0}".format(template_params.get('templateName')) + + response = self.dnac_apply['exec']( + family="configuration_templates", + function=deletion_value, + params=params_key, + ) + task_id = response.get("response").get("taskId") + if task_id: + task_details = self.get_task_details(task_id) + self.result['changed'] = True + self.result['msg'] = task_details.get('progress') + self.result['diff'] = config.get("configuration_templates") + + self.log("Task details for '{0}': {1}".format(deletion_value, task_details), "DEBUG") + self.result['response'] = task_details if task_details else response + if not self.result['msg']: + self.result['msg'] = "Error while deleting {name} : " + self.status = "failed" + return self + + self.msg = "Successfully deleted {0} ".format(name) + self.status = "success" + return self + + def get_diff_deleted(self, config): + """ + Delete projects or templates in Cisco Catalyst Center with fields provided in playbook. + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ + + configuration_templates = config.get("configuration_templates") + if configuration_templates: + is_project_found = self.have_project.get("project_found") + projectName = config.get("configuration_templates").get("project_name") + + if not is_project_found: + self.msg = "Project {0} is not found".format(projectName) + self.status = "failed" + return self + + is_template_found = self.have_template.get("template_found") + template_params = self.want.get("template_params") + templateName = config.get("configuration_templates").get("template_name") + if template_params.get("name"): + if is_template_found: + self.delete_project_or_template(config) + else: + self.msg = "Invalid template {0} under project".format(templateName) + self.status = "failed" + return self + else: + self.log("Template name is empty, deleting the project '{0}' and " + "associated templates" + .format(config.get("configuration_templates").get("project_name")), "INFO") + is_project_deletable = self.have_project.get("isDeletable") + if is_project_deletable: + self.delete_project_or_template(config, is_delete_project=True) + else: + self.msg = "Project is not deletable" + self.status = "failed" + return self + + self.msg = "Successfully completed delete state execution" + self.status = "success" + return self + + def verify_diff_merged(self, config): + """ + Validating the Cisco Catalyst Center configuration with the playbook details + when state is merged (Create/Update). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + if config.get("configuration_templates") is not None: + is_template_available = self.get_have_project(config) + self.log("Template availability: {0}".format(is_template_available), "INFO") + if not is_template_available: + self.msg = "Configuration Template config is not applied to the Cisco Catalyst Center." + self.status = "failed" + return self + + self.get_have_template(config, is_template_available) + self.log("Current State (have): {0}".format(self.want.get("template_params")), "INFO") + self.log("Desired State (want): {0}".format(self.have_template.get("template")), "INFO") + template_params = ["language", "name", "projectName", "softwareType", + "softwareVariant", "templateContent"] + for item in template_params: + if self.have_template.get("template").get(item) != self.want.get("template_params").get(item): + self.msg = " Configuration Template config is not applied to the Cisco Catalyst Center." + self.status = "failed" + return self + self.log("Successfully validated the Template in the Catalyst Center.", "INFO") + self.result.get("response").update({"Validation": "Success"}) + + self.msg = "Successfully validated the Configuration Templates." + self.status = "success" + return self + + def verify_diff_deleted(self, config): + """ + Validating the Cisco Catalyst Center configuration with the playbook details + when state is deleted (delete). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + if config.get("configuration_templates") is not None: + self.log("Current State (have): {0}".format(self.have), "INFO") + self.log("Desired State (want): {0}".format(self.want), "INFO") + template_list = self.dnac_apply['exec']( + family="configuration_templates", + function="gets_the_templates_available", + params={"projectNames": config.get("projectName")}, + ) + if template_list and isinstance(template_list, list): + templateName = config.get("configuration_templates").get("template_name") + template_info = get_dict_result(template_list, + "name", + templateName) + if template_info: + self.msg = "Configuration Template config is not applied to the Cisco Catalyst Center." + self.status = "failed" + return self + + self.log("Successfully validated absence of template in the Catalyst Center.", "INFO") + self.result.get("response").update({"Validation": "Success"}) + + self.msg = "Successfully validated the absence of Template in the Cisco Catalyst Center." + self.status = "success" + return self + + def reset_values(self): + """ + Reset all neccessary attributes to default values. + + Parameters: + self - The current object. + + Returns: + None + """ + + self.have_project.clear() + self.have_template.clear() + self.want.clear() + + +def main(): + """ main entry point for module execution""" + + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + "config_verify": {"type": 'bool', "default": False}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + ccc_template = Template(module) + ccc_template.validate_input().check_return_status() + state = ccc_template.params.get("state") + config_verify = ccc_template.params.get("config_verify") + if state not in ccc_template.supported_states: + ccc_template.status = "invalid" + ccc_template.msg = "State {0} is invalid".format(state) + ccc_template.check_return_status() + + for config in ccc_template.validated_config: + ccc_template.reset_values() + ccc_template.get_have(config).check_return_status() + ccc_template.get_want(config).check_return_status() + ccc_template.get_diff_state_apply[state](config).check_return_status() + if config_verify: + ccc_template.verify_diff_state_apply[state](config).check_return_status() + + module.exit_json(**ccc_template.result) + + +if __name__ == '__main__': + main() From a9ac86a25f8031145c1fa844818a4019f951869d Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 6 Feb 2024 15:24:30 +0530 Subject: [PATCH 02/64] Added the sanity ignore and compile versions; changed the changed the author in template file --- plugins/modules/template_intent.py | 2 +- plugins/modules/template_workflow_manager.py | 8 +++++--- tests/sanity/ignore-2.10.txt | 12 ++++++++++++ tests/sanity/ignore-2.11.txt | 12 ++++++++++++ tests/sanity/ignore-2.12.txt | 12 ++++++++++++ tests/sanity/ignore-2.13.txt | 6 ++++++ tests/sanity/ignore-2.14.txt | 6 ++++++ tests/sanity/ignore-2.15.txt | 6 ++++++ tests/sanity/ignore-2.9.txt | 12 ++++++++++++ 9 files changed, 72 insertions(+), 4 deletions(-) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 4052424087..a0c134d99b 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -__author__ = ['Madhan Sankaranarayanan, Rishita Chowdhary'] +__author__ = ['Madhan Sankaranarayanan, Rishita Chowdhary, Akash Bhaskaran, Muthu Rakesh'] DOCUMENTATION = r""" --- diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index 95ac2ce6e4..7df5c0fb7f 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -__author__ = ['Muthu Rakesh, Madhan Sankaranarayanan'] +__author__ = ['Madhan Sankaranarayanan, Rishita Chowdhary, Akash Bhaskaran, Muthu Rakesh'] DOCUMENTATION = r""" --- @@ -25,8 +25,10 @@ version_added: '6.6.0' extends_documentation_fragment: - cisco.dnac.intent_params -author: Muthu Rakesh (@MUTHU-RAKESH-27) - Madhan Sankaranarayanan (@madhansansel) +author: Madhan Sankaranarayanan (@madhansansel) + Rishita Chowdhary (@rishitachowdhary) + Akash Bhaskaran (@akabhask) + Muthu Rakesh (@MUTHU-RAKESH-27) options: config_verify: description: Set to True to verify the Cisco Catalyst Center after applying the playbook config. diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 4c1b04f143..199ea4ac42 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -728,3 +728,15 @@ plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 2ee339e701..91834bb50a 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -1079,3 +1079,15 @@ plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 98bf9eed14..41aebb287f 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -26,3 +26,15 @@ plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index 0df7244629..29449f7cb2 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -12,3 +12,9 @@ plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 0df7244629..29449f7cb2 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -12,3 +12,9 @@ plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 0df7244629..29449f7cb2 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -12,3 +12,9 @@ plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 4c1b04f143..199ea4ac42 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -728,3 +728,15 @@ plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/template_workflow_manager.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK From da317797448760a0e8bba9cda9daf6dcf5e13fb0 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 6 Feb 2024 15:57:38 +0530 Subject: [PATCH 03/64] API get_image_name_from_id was returning image_id instead of name which makes log messages complicated. --- plugins/modules/swim_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 627cc66417..1e79f45ca2 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -567,7 +567,7 @@ def get_image_name_from_id(self, image_id): self.log(error_message, "ERROR") self.module.fail_json(msg=error_message, response=image_response) - return image_id + return image_name def is_image_exist(self, name): """ From d00b139fc552461787fbbf3c7790a26f114b7222 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 6 Feb 2024 10:31:13 +0000 Subject: [PATCH 04/64] Fixes for SNMP issue and records to return value --- plugins/modules/discovery_intent.py | 69 ++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 3026ca165a..2bccb0e60b 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -429,12 +429,32 @@ def get_dnac_global_credentials_v2_info(self): params=self.validated_config[0].get('headers'), ) response = response.get('response') - cli_len_inp = self.validated_config[0].get("cli_cred_len") - if cli_len_inp > 5: - cli_len_inp = 5 self.log("The Global credentials response from 'get all global credentials v2' API is {0}".format(str(response)), "DEBUG") + + cli_len_inp = self.validated_config[0].get("cli_cred_len") + if response.get("cliCredential") is None: + msg = 'Not found any CLI credentials to perform discovery' + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + if response.get("snmpV2cRead") is None and response.get("snmpV2cWrite") is None and response.get("snmpV3"): + msg = 'Not found any SNMP credentials to perform discovery' + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + total_cli = len(response.get("cliCredential")) + if total_cli > 5: + if cli_len_inp > 5: + cli_len_inp = 5 + + elif total_cli < 6 and cli_len_inp > total_cli: + cli_len_inp = total_cli + cli_len = 0 + for key in response.keys(): + if response[key] is None: + response[key] = [] if key == "cliCredential": for element in response.get(key): while cli_len < cli_len_inp: @@ -442,7 +462,6 @@ def get_dnac_global_credentials_v2_info(self): cli_len += 1 else: self.creds_ids_list.extend(element.get('id') for element in response.get(key)) - if not self.creds_ids_list: msg = 'Not found any credentials to perform discovery' self.log(msg, "CRITICAL") @@ -680,24 +699,42 @@ def lookup_discovery_by_range_via_name(self): of discoveries. If no matching discovery is found, it returns None. """ - params = dict( - start_index=self.validated_config[0].get("start_index"), - records_to_return=self.validated_config[0].get("records_to_return"), - headers=self.validated_config[0].get("headers"), - ) - - response = self.dnac_apply['exec']( - family="discovery", - function='get_discoveries_by_range', - params=params - ) + start_index = self.validated_config[0].get("start_index") + records_to_return = self.validated_config[0].get("records_to_return") + + response = {"response":[]} + if records_to_return > 500: + num_intervals = records_to_return//500 + for num in range(0,num_intervals+1): + params = dict( + start_index=1 + num*500, + records_to_return =500, + headers=self.validated_config[0].get("headers") + ) + response_part = self.dnac_apply['exec']( + family="discovery", + function='get_discoveries_by_range', + params=params + ) + response["response"].extend(response_part["response"]) + else: + params = dict( + start_index=self.validated_config[0].get("start_index"), + records_to_return=self.validated_config[0].get("records_to_return"), + headers=self.validated_config[0].get("headers"), + ) + response = self.dnac_apply['exec']( + family="discovery", + function='get_discoveries_by_range', + params=params + ) self.log("Response of the get discoveries via range API is {0}".format(str(response)), "DEBUG") return next( filter( lambda x: x['name'] == self.validated_config[0].get('discovery_name'), - response.response + response.get("response") ), None ) From 0e2f027a9d17d16813f3d5bcc9b6aedd2e3db210 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 6 Feb 2024 11:05:57 +0000 Subject: [PATCH 05/64] Fixes for SNMP issue and records to return value --- plugins/modules/discovery_intent.py | 66 ++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 2bccb0e60b..e5fe2cd82e 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -70,9 +70,35 @@ http_read_credential: description: HTTP read credentials for hosting a device type: dict + suboptions: + password: + description: HTTP(S) password and is mandatory for using HTTP credentials. + type: str + port: + description: HTTP(S) port and is mandatory for using HTTP credentials. + type: int + secure: + description: Flag for HTTP(S) and is not mandatory for using HTTP credentials. + type: bool + username: + description: HTTP(S) username and is mandatory for using HTTP credentials. + type: str http_write_credential: description: HTTP write credentials for hosting a device type: dict + suboptions: + password: + description: HTTP(S) password and is mandatory for using HTTP credentials. + type: str + port: + description: HTTP(S) port and is mandatory for using HTTP credentials. + type: int + secure: + description: Flag for HTTP(S) and is not mandatory for using HTTP credentials. + type: bool + username: + description: HTTP(S) username and is mandatory for using HTTP credentials. + type: str ip_filter_list: description: List of IP adddrsess that needs to get filtered out from the IP addresses added type: list @@ -195,8 +221,8 @@ start_index: integer enable_password_list: list records_to_return: integer - http_read_credential: dict - http_write_credential: dict + http_read_credential: dictionary + http_write_credential: dictionary ip_filter_list: list discovery_name: string password_list: list @@ -567,6 +593,32 @@ def create_params(self, credential_ids=None, ip_address_list=None): if credential_ids is None: credential_ids = [] + http_read_credential = self.validated_config[0].get('http_read_credential') + http_write_credential = self.validated_config[0].get('http_write_credential') + if http_read_credential: + if not (http_read_credential.get('password') and isinstance(http_read_credential.get('password'), str)): + msg = "Http Read Credential must have a password of type string" + if not (http_read_credential.get('username') and isinstance(http_read_credential.get('username'), str)): + msg = "Http Read Credential must have a username of type string" + if not (http_read_credential.get('password') and isinstance(http_read_credential.get('password'), int)): + msg = "Http Read Credential must have port of type integer" + if not isinstance(http_read_credential.get('secure'), bool): + msg = "Secure must be of type bool" + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + if http_write_credential: + if not (http_write_credential.get('password') and isinstance(http_read_credential.get('password'), str)): + msg = "Http Write Credential must have a password of type string" + if not (http_write_credential.get('username') and isinstance(http_read_credential.get('username'), str)): + msg = "Http Write Credential must have a username of type string" + if not (http_write_credential.get('password') and isinstance(http_read_credential.get('password'), int)): + msg = "Http Write Credential must have port of type integer" + if not isinstance(http_write_credential.get('secure'), bool): + msg = "Secure must be of type bool" + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + new_object_params = {} new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') new_object_params['discoveryType'] = self.validated_config[0].get('discovery_type') @@ -702,13 +754,13 @@ def lookup_discovery_by_range_via_name(self): start_index = self.validated_config[0].get("start_index") records_to_return = self.validated_config[0].get("records_to_return") - response = {"response":[]} + response = {"response": []} if records_to_return > 500: - num_intervals = records_to_return//500 - for num in range(0,num_intervals+1): + num_intervals = records_to_return // 500 + for num in range(0, num_intervals + 1): params = dict( - start_index=1 + num*500, - records_to_return =500, + start_index=1 + num * 500, + records_to_return=500, headers=self.validated_config[0].get("headers") ) response_part = self.dnac_apply['exec']( From 64cfc22568d9f8713a8d0512d68342783ee4ec94 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 6 Feb 2024 17:02:58 +0530 Subject: [PATCH 06/64] Used name and description for identying the device credentials rather than id --- .../device_credential_workflow_manager.yml | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/playbooks/device_credential_workflow_manager.yml b/playbooks/device_credential_workflow_manager.yml index 2955ec64d1..08a7a97951 100644 --- a/playbooks/device_credential_workflow_manager.yml +++ b/playbooks/device_credential_workflow_manager.yml @@ -27,17 +27,14 @@ enable_password: '12345' # old_description: # old_username: - # id: e448ea13-4de0-406b-bc6e-f72b57ed6746 # Use this for updation or deletion snmp_v2c_read: - description: SNMPv2c Read1 # use this for deletion read_community: '123456' # old_description: # use this for updating the description - # id: 0ee7d677-8804-43f2-8b6c-599c5f18348f # Use this for updation or deletion snmp_v2c_write: - description: SNMPv2c Write1 # use this for deletion write_community: '123456' # old_description: # use this for updating the description - # id: a96abc1b-1fd6-41f1-8a6d-a5569c17262d # Use this for updation or deletion snmp_v3: - auth_password: '12345678' # Atleast 8 characters auth_type: SHA # [SHA, MD5] (SHA is recommended) @@ -47,7 +44,6 @@ username: snmpV31 description: snmpV31 # old_description: - # id: d8974823-250a-41b0-8c9b-b27b2ae01472 # Use this for updation or deletion https_read: - description: HTTP Read1 username: HTTP_Read1 @@ -55,7 +51,6 @@ port: 443 # old_description: # old_username: - # id: a7ef9995-e404-4240-94ca-b5f37f65c19d # Use this for updation or deletion https_write: - description: HTTP Write1 username: HTTP_Write1 @@ -63,29 +58,22 @@ port: 443 # old_description: # old_username: - # id: bec9818e-30cd-468b-bf75-292beefc2e20 # Use this for updation or deletion assign_credentials_to_site: cli_credential: - # description: CLI - # username: cli - id: 2fc5f7d4-cf15-4a4f-99b3-f086e8dd6350 + description: CLI + username: cli snmp_v2c_read: - # description: SNMPv2c Read - id: a966a4e5-9d11-4683-8edc-a5ad8fa59ee3 + description: SNMPv2c Read snmp_v2c_write: - # description: SNMPv2c Write - id: 7cd072a4-2263-4087-b6ec-93b20958e286 + description: SNMPv2c Write snmp_v3: - # description: snmpV3 - id: c08a1797-84ce-4add-94a3-b419b13621e4 + description: snmpV3 https_read: - # description: HTTP Read - # username: HTTP_Read - id: 1009725d-373b-4e7c-a091-300777e2bbe2 + description: HTTP Read + username: HTTP_Read https_write: - # description: HTTP Write - # username: HTTP_Write - id: f1ab6e3d-01e9-4d87-8271-3ac5fde83980 + description: HTTP Write + username: HTTP_Write site_name: - Global/Chennai/Trill - Global/Chennai/Tidel From 90b95be8450e802778aee8627d5774cafbaa8b3f Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 6 Feb 2024 20:24:21 +0530 Subject: [PATCH 07/64] Make seperate file workflow_manager file for each module of Swim, Site and Inventory, added playbooks for same --- playbooks/inventory_workflow_manager.yml | 64 + playbooks/site_workflow_manager.yml | 47 + playbooks/swim_workflow_manager.yml | 54 + plugins/modules/inventory_workflow_manager.py | 3354 +++++++++++++++++ plugins/modules/site_workflow_manager.py | 1063 ++++++ plugins/modules/swim_workflow_manager.py | 1717 +++++++++ 6 files changed, 6299 insertions(+) create mode 100644 playbooks/inventory_workflow_manager.yml create mode 100644 playbooks/site_workflow_manager.yml create mode 100644 playbooks/swim_workflow_manager.yml create mode 100644 plugins/modules/inventory_workflow_manager.py create mode 100644 plugins/modules/site_workflow_manager.py create mode 100644 plugins/modules/swim_workflow_manager.py diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml new file mode 100644 index 0000000000..8f7028eca4 --- /dev/null +++ b/playbooks/inventory_workflow_manager.yml @@ -0,0 +1,64 @@ +--- +- name: Configure device credentials on Cisco DNA Center + hosts: localhost + connection: local + gather_facts: no + vars_files: + - "input_inventory.yml" + - "credentials.yml" + tasks: + - name: Add/Update/Resync/Delete the devices in Cisco DNA Center Inventory. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: DEBUG + dnac_log: true + config_verify: true + state: merged + config: + - username: "{{item.username}}" + enable_password: "{{item.enable_password}}" + password: "{{item.password}}" + ip_address: "{{item.ip_address}}" + cli_transport: "{{item.cli_transport}}" + # hostname_list: "{{item.hostname_list}}" + # serial_number_list: "{{item.serial_number_list}}" + # mac_address_list: "{{item.mac_address_list}}" + snmp_auth_passphrase: "{{item.snmp_auth_passphrase}}" + snmp_auth_protocol: "{{item.snmp_auth_protocol}}" + snmp_mode: "{{item.snmp_mode}}" + snmp_priv_passphrase: "{{item.snmp_priv_passphrase}}" + snmp_priv_protocol: "{{item.snmp_priv_protocol}}" + snmp_ro_community: "{{item.snmp_ro_community}}" + snmp_rw_community: "{{item.snmp_rw_community}}" + snmp_username: "{{item.snmp_username}}" + device_updated: "{{item.device_updated}}" + credential_update: "{{item.credential_update}}" + clean_config: "{{item.clean_config}}" + type: "{{item.type}}" + device_added: "{{item.device_added}}" + device_resync: "{{item.device_resync}}" + reboot_device: "{{item.reboot_device}}" + update_device_role: + role: "{{item.role}}" + role_source: "{{item.role_source}}" + add_user_defined_field: + name: "{{item.name}}" + description: "{{item.description}}" + value: "{{item.value}}" + provision_wired_device: + site_name: "{{item.site_name}}" + update_interface_details: + description: "{{item.update_interface_details.description}}" + interface_name: "{{item.interface_name}}" + export_device_list: + password: "{{item.export_device_list.password}}" + + with_items: "{{ device_details }}" + tags: + - inventory_device diff --git a/playbooks/site_workflow_manager.yml b/playbooks/site_workflow_manager.yml new file mode 100644 index 0000000000..b35ad4a8d1 --- /dev/null +++ b/playbooks/site_workflow_manager.yml @@ -0,0 +1,47 @@ +- hosts: localhost + connection: local + gather_facts: no + vars_files: + - "credentials.yml" + tasks: + - name: Get site info and updating site details + cisco.dnac.site_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: true + dnac_log_level: DEBUG + config_verify: true + state: merged + config: + - site: + floor: + name: Test_Floor6 + parent_name: 'Global/USA/San Francisco/BGL_18' + length: "103" + width: "75" + height: "50" + rf_model: 'Cubes And Walled Offices' + floor_number: 3 + type: floor + - site: + area: + name: Abc + parent_name: 'Global' + address: Bengaluru, Karnataka, India + latitude: 22.2111 + longitude: -42.1234434 + country: "United States" + type: area + + # For deleting a site + # - site: + # floor: + # name: Test_Floor2 + # parent_name: 'Global/USA/San Francisco/BGL_18' + # type: floor + diff --git a/playbooks/swim_workflow_manager.yml b/playbooks/swim_workflow_manager.yml new file mode 100644 index 0000000000..d09cdce00e --- /dev/null +++ b/playbooks/swim_workflow_manager.yml @@ -0,0 +1,54 @@ +--- +- name: Configure device credentials on Cisco DNA Center + hosts: localhost + connection: local + gather_facts: no + vars_files: + - "input_swim.yml" + - "credentials.yml" + tasks: + - name: Import an image, tag it as golden and load it on device + cisco.dnac.swim_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: true + dnac_log_level: DEBUG + config_verify: true + config: + - import_image_details: + # type: "local" + # local_image_details: + # file_path: "/Users/abmahesh/Downloads/cat9k_iosxe.17.12.01.SPA.bin" + # is_third_party: false + type: "{{ item.type }}" + url_details: + payload: + - source_url: http://10.197.156.28/stda/abimishr/cat9k_iosxe.17.12.01.SPA.bin + third_party: False + tagging_details: + image_name: "{{item.image_name}}" + site_name: "{{item.site_name}}" + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + device_type: "{{item.device_type}}" + tagging: false + image_distribution_details: + image_name: "{{item.image_name}}" + site_name: "{{item.site_name}}" + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + image_activation_details: + site_name: "{{item.site_name}}" + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + scehdule_validate: false + distribute_if_needed: true + + with_items: "{{ image_details }}" + tags: + - swim \ No newline at end of file diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py new file mode 100644 index 0000000000..f5ac9ead76 --- /dev/null +++ b/plugins/modules/inventory_workflow_manager.py @@ -0,0 +1,3354 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ("Madhan Sankaranarayanan, Abhishek Maheshwari") + +DOCUMENTATION = r""" +--- +module: inventory_workflow_manager +short_description: Resource module for Network Device +description: +- Manage operations create, update and delete of the resource Network Device. +- Adds the device with given credential. +- Deletes the network device for the given Id. +- Sync the devices provided as input. +version_added: '6.8.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Abhishek Maheshwari (@abmahesh) + Madhan Sankaranarayanan (@madhansansel) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. + type: bool + default: False + state: + description: The state of Cisco Catalyst Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: List of devices with credentails to perform Add/Update/Delete/Resync operation + type: list + elements: dict + required: True + suboptions: + cli_transport: + description: The essential prerequisite for adding Network devices is the specification of the transport + protocol (either SSH or Telnet) used by the device. + type: str + compute_device: + description: Compute Device flag. + type: bool + enable_password: + description: Device's enable password. + type: str + extended_discovery_info: + description: Device's extended discovery info. + type: str + http_password: + description: Device's http password. Required for Adding Compute, Meraki, Firepower Management Devices. + type: str + http_port: + description: Device's http port number. Required for Adding Compute, Firepower Management Devices. + type: str + http_secure: + description: HttpSecure flag. + type: bool + http_username: + description: Device's http username. Required for Adding Compute,Firepower Management Devices. + type: str + ip_address: + description: Device's ipAddress. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. + elements: str + type: list + hostname_list: + description: "A list of hostnames representing devices. Operations such as updating, deleting, resyncing, or rebooting + can be performed as alternatives to using IP addresses." + elements: str + type: list + serial_number_list: + description: A list of serial numbers representing devices. Operations such as updating, deleting, resyncing, or rebooting + can be performed as alternatives to using IP addresses. + elements: str + type: list + mac_address_list: + description: "A list of MAC addresses representing devices. Operations such as updating, deleting, resyncing, or rebooting + can be performed as alternatives to using IP addresses." + elements: str + type: list + netconf_port: + description: Device's netconf port. + type: str + username: + description: Network Device's username. Required for Adding Network Device. + type: str + password: + description: Device's password. Required for Adding Network Device. + Also needed for file encryption while exporting device in a csv file. + type: str + serial_number: + description: Device's serial number. + type: str + snmp_auth_passphrase: + description: Device's snmp auth passphrase. Required for Adding Network, Compute, Third Party Devices. + type: str + snmp_auth_protocol: + description: Device's snmp Auth Protocol. + type: str + default: "SHA" + snmp_mode: + description: Device's snmp Mode. + type: str + default: "AUTHPRIV" + snmp_priv_passphrase: + description: Device's snmp Private Passphrase. Required for Adding Network, Compute, Third Party Devices. + type: str + snmp_priv_protocol: + description: Device's snmp Private Protocol. Required for Adding Network, Compute, Third Party Devices. + Must be given in playbook if you are updating the device credentails. + type: str + snmp_ro_community: + description: Device's snmp ROCommunity. Required for Adding V2C Devices. + type: str + default: public + snmp_rw_community: + description: Device's snmp RWCommunity. Required for Adding V2C Devices. + type: str + default: private + snmp_retry: + description: Device's snmp Retry. + type: int + default: 3 + snmp_timeout: + description: Device's snmp Timeout. + type: int + default: 5 + snmp_username: + description: Device's snmp Username. Required for Adding Network, Compute, Third Party Devices. + type: str + snmp_version: + description: Device's snmp Version. + type: str + default: "v3" + type: + description: Select Device's type from NETWORK_DEVICE, COMPUTE_DEVICE, MERAKI_DASHBOARD, THIRD_PARTY_DEVICE, FIREPOWER_MANAGEMENT_SYSTEM. + type: str + default: "NETWORK_DEVICE" + update_mgmt_ipaddresslist: + description: Network Device's update Mgmt IPaddress List. + type: list + elements: dict + suboptions: + exist_mgmt_ipaddress: + description: Device's existing Mgmt IpAddress. + type: str + new_mgmt_ipaddress: + description: Device's new Mgmt IpAddress. + type: str + force_sync: + description: If forcesync is true then device sync would run in high priority thread if available, else the sync will fail. + type: bool + default: false + device_added: + description: Make this as true needed for the addition of device in inventory. + type: bool + default: false + device_updated: + description: Make this as true needed for the updation of device role, interface details, device credentails or details. + type: bool + default: false + device_resync: + description: Make this as true needed for the resyncing of device. + type: bool + default: false + reboot_device: + description: Make this as true needed for the Rebooting of Access Points. + type: bool + default: false + credential_update: + description: Make this as true needed for the updation of device credentials and other device details. + type: bool + default: false + clean_config: + description: Required if need to delete the Provisioned device by clearing current configuration. + type: bool + default: false + role: + description: Role of device which can be ACCESS, CORE, DISTRIBUTION, BORDER ROUTER, UNKNOWN. + type: str + default: "ACCESS" + role_source: + description: role source for the Device. + type: str + default: "AUTO" + name: + description: Name of Global User Defined Field. Required for creating/deleting UDF and then assigning it to device. + type: str + description: + description: Info about the global user defined field. Also used while updating interface details. + type: str + value: + description: Value to assign to tag with or without the same user defined field name. + type: str + admin_status: + description: Status of Interface of a device, it can be (UP/DOWN). + type: str + vlan_id: + description: Unique Id number assigned to a VLAN within a network. + type: int + voice_vlan_id: + description: Identifier used to distinguish a specific VLAN that is dedicated to voice traffic. + type: int + interface_name: + description: Specify the interface name to update the details of the device interface. (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) + deployment_mode: + description: Preview/Deploy [Preview means the configuration is not pushed to the device. Deploy makes the configuration pushed to the device] + type: str + default: "Deploy" + site_name: + description: Required for Provisioning of Wired and Wireless Devices. + type: str + operation_enum: + description: enum(CREDENTIALDETAILS, DEVICEDETAILS) 0 to export Device Credential Details Or 1 to export Device Details. + type: str + parameters: + description: List of device parameters that needs to be exported to file. + type: str + managed_ap_locations: + description: Location of the sites allocated for the APs + type: list + elements: str + dynamic_interfaces: + description: Interface details of the wireless device + type: list + elements: dict + suboptions: + interface_ip_address: + description: Ip Address allocated to the interface + type: str + interface_netmask_in_cidr: + description: The netmask of the interface, given in CIDR notation. This is an integer that represents the + number of bits set in the netmask + type: int + interface_gateway: + description: The name identifier for the gateway associated with the interface. + type: str + lag_or_port_number: + description: The Link Aggregation Group (LAG) number or port number assigned to the interface. + type: int + vlan_id: + description: The VLAN (Virtual Local Area Network) ID associated with the network interface. + type: int + interface_name: + description: Name of the interface. + type: str + +requirements: +- dnacentersdk >= 2.5.5 +- python >= 3.5 +seealso: +- name: Cisco Catalyst Center documentation for Devices AddDevice2 + description: Complete reference of the AddDevice2 API. + link: https://developer.cisco.com/docs/dna-center/#!add-device +- name: Cisco Catalyst Center documentation for Devices DeleteDeviceById + description: Complete reference of the DeleteDeviceById API. + link: https://developer.cisco.com/docs/dna-center/#!delete-device-by-id +- name: Cisco Catalyst Center documentation for Devices SyncDevices2 + description: Complete reference of the SyncDevices2 API. + link: https://developer.cisco.com/docs/dna-center/#!sync-devices +notes: + - SDK Method used are + devices.Devices.add_device, + devices.Devices.delete_device_by_id, + devices.Devices.sync_devices, + + - Paths used are + post /dna/intent/api/v1/network-device, + delete /dna/intent/api/v1/network-device/{id}, + put /dna/intent/api/v1/network-device, + + - Removed 'managementIpAddress' options in v4.3.0. +""" + +EXAMPLES = r""" +- name: Add new device in Inventory with full credentials + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - cli_transport: string + compute_device: false + enable_password: string + extended_discovery_info: string + http_password: string + http_port: string + http_secure: false + http_username: string + ip_address: + - string + netconf_port: string + password: string + serial_number: string + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_ro_community: string + snmp_rw_community: string + snmp_retry: 3 + snmp_timeout: 5 + snmp_username: string + snmp_version: string + type: string + device_added: true + username: string + +- name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + http_username: string + http_password: string + http_port: string + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_retry: 3 + snmp_timeout: 5 + snmp_username: string + compute_device: true + username: string + device_added: true + type: "COMPUTE_DEVICE" + +- name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - http_password: string + device_added: true + type: "MERAKI_DASHBOARD" + +- name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + http_username: string + http_password: string + http_port: string + device_added: true + type: "FIREPOWER_MANAGEMENT_SYSTEM" + +- name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_retry: 3 + snmp_timeout: 5 + snmp_username: string + device_added: true + type: "THIRD_PARTY_DEVICE" + +- name: Update device details or credentails in Inventory + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - cli_transport: string + compute_device: false + password: string + enable_password: string + extended_discovery_info: string + http_password: string + http_port: string + http_secure: false + http_username: string + ip_address: + - string + netconf_port: string + serial_number: string + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_username: string + snmp_version: string + type: string + device_updated: true + credential_update: true + update_mgmt_ipaddresslist: + - exist_mgmt_ipaddress: string + new_mgmt_ipaddress: string + username: string + +- name: Update new management IP address of device in inventory + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - device_updated: true + ip_address: + - string + credential_update: true + update_mgmt_ipaddresslist: + - exist_mgmt_ipaddress: string + new_mgmt_ipaddress: string + +- name: Associate Wired Devices to site and Provisioned it in Inventory + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + provision_wired_device: + site_name: string + +- name: Associate Wireless Devices to site and Provisioned it in Inventory + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + provision_wireless_device: + - site_name: string + managed_ap_locations: + - string + dynamic_interfaces: + - interface_ip_address: string + interface_netmask_in_cidr: int + interface_gateway: string + lag_or_port_number: int + vlan_id: int + interface_name: string + +- name: Update Device Role with IP Address + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + device_updated: true + update_device_role: + role: string + role_source: string + +- name: Update Interface details with IP Address + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + device_updated: true + update_interface_details: + description: str + admin_status: str + vlan_id: int + voice_vlan_id: int + deployment_mode: str + interface_name: str + +- name: Export Device Details in a CSV file Interface details with IP Address + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + export_device_list: + password: str + operation_enum: str + parameters: str + +- name: Create Global User Defined with IP Address + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + add_user_defined_field: + name: string + description: string + value: string + +- name: Resync Device with IP Addresses + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + device_resync: true + force_sync: false + +- name: Reboot AP Devices with IP Addresses + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address: + - string + reboot_device: true + +- name: Delete Provision/Unprovision Devices by IP Address + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: False + dnac_log_level: "{{dnac_log_level}}" + state: deleted + config: + - ip_address: + - string + clean_config: false + +- name: Delete Global User Defined Field with name + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: deleted + config: + - ip_address: + - string + add_user_defined_field: + name: string + +""" + +RETURN = r""" + +dnac_response: + description: A dictionary or list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": { + "taskId": "string", + "url": "string" + }, + "version": "string" + } +""" +# common approach when a module relies on optional dependencies that are not available during the validation process. +try: + import pyzipper + HAS_PYZIPPER = True +except ImportError: + HAS_PYZIPPER = False + pyzipper = None + +import csv +from datetime import datetime +from io import BytesIO, StringIO +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, +) + + +class Inventory(DnacBase): + """Class containing member attributes for inventory workflow manager module""" + + def __init__(self, module): + super().__init__(module) + self.supported_states = ["merged", "deleted"] + + def validate_input(self): + """ + Validate the fields provided in the playbook. + Checks the configuration provided in the playbook against a predefined specification + to ensure it adheres to the expected structure and data types. + Parameters: + self: The instance of the class containing the 'config' attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of the 'config' parameter. + Example: + To use this method, create an instance of the class and call 'validate_input' on it. + If the validation succeeds, 'self.status' will be 'success' and 'self.validated_config' + will contain the validated configuration. If it fails, 'self.status' will be 'failed', and + 'self.msg' will describe the validation issues. + """ + + temp_spec = { + 'cli_transport': {'type': 'str'}, + 'compute_device': {'type': 'bool'}, + 'enable_password': {'type': 'str'}, + 'extended_discovery_info': {'type': 'str'}, + 'http_password': {'type': 'str'}, + 'http_port': {'type': 'str'}, + 'http_secure': {'type': 'bool'}, + 'http_username': {'type': 'str'}, + 'ip_address': {'type': 'list', 'elements': 'str'}, + 'hostname_list': {'type': 'list', 'elements': 'str'}, + 'serial_number_list': {'type': 'list', 'elements': 'str'}, + 'mac_address_list': {'type': 'list', 'elements': 'str'}, + 'netconf_port': {'type': 'str'}, + 'password': {'type': 'str'}, + 'serial_number': {'type': 'str'}, + 'snmp_auth_passphrase': {'type': 'str'}, + 'snmp_auth_protocol': {'default': "SHA", 'type': 'str'}, + 'snmp_mode': {'default': "AUTHPRIV", 'type': 'str'}, + 'snmp_priv_passphrase': {'type': 'str'}, + 'snmp_priv_protocol': {'type': 'str'}, + 'snmp_ro_community': {'default': "public", 'type': 'str'}, + 'snmp_rw_community': {'default': "private", 'type': 'str'}, + 'snmp_retry': {'default': 3, 'type': 'int'}, + 'snmp_timeout': {'default': 5, 'type': 'int'}, + 'snmp_username': {'type': 'str'}, + 'snmp_version': {'default': "v3", 'type': 'str'}, + 'update_mgmt_ipaddresslist': {'type': 'list', 'elements': 'dict'}, + 'username': {'type': 'str'}, + 'update_device_role': {'type': 'dict'}, + 'device_added': {'type': 'bool'}, + 'device_updated': {'type': 'bool'}, + 'device_resync': {'type': 'bool'}, + 'reboot_device': {'type': 'bool'}, + 'credential_update': {'type': 'bool'}, + 'force_sync': {'type': 'bool'}, + 'clean_config': {'type': 'bool'}, + 'add_user_defined_field': { + 'type': 'dict', + 'name': {'type': 'str'}, + 'description': {'type': 'str'}, + 'value': {'type': 'str'}, + }, + 'update_interface_details': { + 'type': 'dict', + 'description': {'type': 'str'}, + 'vlan_id': {'type': 'int'}, + 'voice_vlan_id': {'type': 'int'}, + 'interface_name': {'type': 'str'}, + }, + 'export_device_list': { + 'type': 'dict', + 'password': {'type': 'str'}, + 'operation_enum': {'type': 'str'}, + 'parameters': {'type': 'str'}, + }, + 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, + 'provision_wired_device': {'type': 'dict'}, + 'provision_wireless_device': { + 'type': 'list', + 'site_name': {'type': 'str'}, + 'managed_ap_locations': {'type': 'list', 'elements': 'str'}, + 'dynamic_interfaces': { + 'type': 'list', + 'interface_ip_address': {'type': 'str'}, + 'interface_netmask_in_cidr': {'type': 'int'}, + 'interface_gateway': {'type': 'str'}, + 'lag_or_port_number': {'type': 'int'}, + 'vlan_id': {'type': 'int'}, + 'interface_name': {'type': 'str'}, + }, + } + } + + # Validate device params + valid_temp, invalid_params = validate_list_of_dicts( + self.config, temp_spec + ) + + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.log(self.msg, "ERROR") + self.status = "failed" + return self + + self.validated_config = valid_temp + self.msg = "Successfully validated playbook configuration parameters using 'validate_input': {0}".format(str(valid_temp)) + self.log(self.msg, "INFO") + self.status = "success" + + return self + + def get_device_ips_from_config_priority(self): + """ + Retrieve device IPs based on the configuration. + Args: + - self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. + Returns: + list: A list containing device IPs. + Description: + This method retrieves device IPs based on the priority order specified in the configuration. + It first checks if device IPs are available. If not, it checks hostnames, serial numbers, + and MAC addresses in order and retrieves IPs based on availability. + If none of the information is available, an empty list is returned. + """ + # Retrieve device IPs from the configuration + device_ips = self.config[0].get("ip_address") + + if device_ips: + return device_ips + + # If device IPs are not available, check hostnames + device_hostnames = self.config[0].get("hostname_list") + if device_hostnames: + return self.get_device_ips_from_hostname(device_hostnames) + + # If hostnames are not available, check serial numbers + device_serial_numbers = self.config[0].get("serial_number_list") + if device_serial_numbers: + return self.get_device_ips_from_serial_number(device_serial_numbers) + + # If serial numbers are not available, check MAC addresses + device_mac_addresses = self.config[0].get("mac_address_list") + if device_mac_addresses: + return self.get_device_ips_from_mac_address(device_mac_addresses) + + # If no information is available, return an empty list + return [] + + def device_exists_in_ccc(self): + """ + Check which devices already exists in Cisco Catalyst Center and return both device_exist and device_not_exist in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. + Returns: + list: A list of devices that exist in Cisco Catalyst Center. + Description: + Queries Cisco Catalyst Center to check which devices are already present in Cisco Catalyst Center and store + its management IP address in the list of devices that exist. + Example: + To use this method, create an instance of the class and call 'device_exists_in_ccc' on it, + The method returns a list of management IP addressesfor devices that exist in Cisco Catalyst Center. + """ + + device_in_ccc = [] + + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + ) + + except Exception as e: + error_message = "Error while fetching device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "CRITICAL") + raise Exception(error_message) + + if response: + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + for ip in response: + device_ip = ip["managementIpAddress"] + device_in_ccc.append(device_ip) + + return device_in_ccc + + def is_udf_exist(self, field_name): + """ + Check if a Global User Defined Field exists in Cisco Catalyst Center based on its name. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + field_name (str): The name of the Global User Defined Field. + Returns: + bool: True if the Global User Defined Field exists, False otherwise. + Description: + The function sends a request to Cisco Catalyst Center to retrieve all Global User Defined Fields + with the specified name. If matching field is found, the function returns True, indicating that + the field exists else returns False. + """ + + response = self.dnac._exec( + family="devices", + function='get_all_user_defined_fields', + params={"name": field_name}, + ) + + self.log("Received API response from 'get_all_user_defined_fields': {0}".format(str(response)), "DEBUG") + udf = response.get("response") + + if (len(udf) == 1): + return True + + message = "Global User Defined Field with name '{0}' doesnot exist in Cisco Catalyst Center".format(field_name) + self.log(message, "INFO") + + return False + + def create_user_defined_field(self): + """ + Create a Global User Defined Field in Cisco Catalyst Center based on the provided configuration. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + The function retrieves the configuration for adding a user-defined field from the configuration object, + sends the request to Cisco Catalyst Center to create the field, and logs the response. + """ + try: + payload = self.config[0].get('add_user_defined_field') + response = self.dnac._exec( + family="devices", + function='create_user_defined_field', + params=payload, + ) + self.log("Received API response from 'create_user_defined_field': {0}".format(str(response)), "DEBUG") + response = response.get("response") + field_name = self.config[0].get('add_user_defined_field').get('name') + self.log("Global User Defined Field with name '{0}' created successfully".format(field_name), "INFO") + self.status = "success" + + except Exception as e: + error_message = "Error while creating Global UDF(User Defined Field) in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + + return self + + def add_field_to_devices(self, device_ids): + """ + Add a Global user-defined field with specified details to a list of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ids (list): A list of device IDs to which the user-defined field will be added. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + The function retrieves the details of the user-defined field from the configuration object, + including the field name and default value then iterates over list of device IDs, creating a payload for + each device and sending the request to Cisco Catalyst Center to add the user-defined field. + """ + field_details = self.config[0].get('add_user_defined_field') + field_name = field_details.get('name') + field_value = field_details.get('value', '1') + for device_id in device_ids: + payload = {} + payload['name'] = field_name + payload['value'] = field_value + udf_param_dict = { + 'payload': [payload], + 'device_id': device_id + } + try: + response = self.dnac._exec( + family="devices", + function='add_user_defined_field_to_device', + params=udf_param_dict, + ) + self.log("Received API response from 'add_user_defined_field_to_device': {0}".format(str(response)), "DEBUG") + response = response.get("response") + self.status = "success" + self.result['changed'] = True + + except Exception as e: + self.status = "failed" + error_message = "Error while adding Global UDF to device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + self.result['changed'] = False + + return self + + def trigger_export_api(self, payload_params): + """ + Triggers the export API to generate a CSV file containing device details based on the given payload parameters. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + payload_params (dict): A dictionary containing parameters required for the export API. + Returns: + dict: The response from the export API, including information about the task and file ID. + If the export is successful, the CSV file can be downloaded using the file ID. + Description: + The function initiates the export API in Cisco Catalyst Center to generate a CSV file containing detailed information + about devices.The response from the API includes task details and a file ID. + """ + + response = self.dnac._exec( + family="devices", + function='export_device_list', + op_modifies=True, + params=payload_params, + ) + self.log("Received API response from 'export_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + task_id = response.get("taskId") + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("additionalStatusURL"): + file_id = execution_details.get("additionalStatusURL").split("/")[-1] + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Could not get the File ID because of {0} so can't export device details in csv file".format(failure_reason) + else: + self.msg = "Could not get the File ID so can't export device details in csv file" + self.log(self.msg, "ERROR") + + return response + + # With this File ID call the Download File by FileID API and process the response + response = self.dnac._exec( + family="file", + function='download_a_file_by_fileid', + op_modifies=True, + params={"file_id": file_id}, + ) + self.log("Received API response from 'download_a_file_by_fileid': {0}".format(str(response)), "DEBUG") + + return response + + def decrypt_and_read_csv(self, response, password): + """ + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + response (requests.Response): HTTP response object containing the encrypted CSV file. + password (str): Password used for decrypting the CSV file. + Returns: + csv.DictReader: A CSV reader object for the decrypted content, allowing iteration over rows as dictionaries. + Description: + Decrypts and reads a CSV-like file from the given HTTP response using the provided password. + """ + + zip_data = BytesIO(response.data) + + if not HAS_PYZIPPER: + self.msg = "pyzipper is required for this module. Install pyzipper to use this functionality." + self.log(self.msg, "CRITICAL") + self.status = "failed" + return self + + snmp_protocol = self.config[0].get('snmp_priv_protocol', 'AES128') + encryption_dict = { + 'AES128': 'pyzipper.WZ_AES128', + 'AES192': 'pyzipper.WZ_AES192', + 'AES256': 'pyzipper.WZ_AES', + 'CISCOAES128': 'pyzipper.WZ_AES128', + 'CISCOAES192': 'pyzipper.WZ_AES192', + 'CISCOAES256': 'pyzipper.WZ_AES' + } + try: + encryption_method = encryption_dict.get(snmp_protocol) + except Exception as e: + self.log("Given SNMP protcol '{0}' not present".format(snmp_protocol), "WARNING") + + if not encryption_method: + self.msg = "Invalid SNMP protocol '{0}' specified for encryption.".format(snmp_protocol) + self.log(self.msg, "ERROR") + self.status = "failed" + return self + + # Create a PyZipper object with the password + with pyzipper.AESZipFile(zip_data, 'r', compression=pyzipper.ZIP_LZMA, encryption=encryption_method) as zip_ref: + # Assuming there is a single file in the zip archive + file_name = zip_ref.namelist()[0] + + # Extract the content of the file with the provided password + file_content_binary = zip_ref.read(file_name, pwd=password.encode('utf-8')) + + # Now 'file_content_binary' contains the binary content of the decrypted file + # Since the content is text, so we can decode it + file_content_text = file_content_binary.decode('utf-8') + + # Now 'file_content_text' contains the text content of the decrypted file + self.log("Text content of decrypted file: {0}".format(file_content_text), "DEBUG") + + # Parse the CSV-like string into a list of dictionaries + csv_reader = csv.DictReader(StringIO(file_content_text)) + + return csv_reader + + def export_device_details(self): + """ + Export device details from Cisco Catalyst Center into a CSV file. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of the class with updated result, status, and log. + Description: + This function exports device details from Cisco Catalyst Center based on the provided IP addresses in the configuration. + It retrieves the device UUIDs, calls the export device list API, and downloads the exported data of both device details and + and device credentials with an encrtypted zip file with password into CSV format. + The CSV data is then parsed and written to a file. + """ + + device_ips = self.get_device_ips_from_config_priority() + + if not device_ips: + self.status = "failed" + self.msg = "Cannot export device details as no devices are specified in the playbook" + self.log(self.msg, "ERROR") + return self + + try: + device_uuids = self.get_device_ids(device_ips) + + if not device_uuids: + self.status = "failed" + self.result['changed'] = False + self.msg = "Could not find device UUIDs for exporting device details" + self.log(self.msg, "ERROR") + return self + + # Now all device UUID get collected so call the export device list API + export_device_list = self.config[0].get('export_device_list') + password = export_device_list.get("password") + + if not self.is_valid_password(password): + self.status = "failed" + detailed_msg = """Invalid password. Min password length is 8 and it should contain atleast one lower case letter, + one uppercase letter, one digit and one special characters from -=\\;,./~!@#$%^&*()_+{}[]|:?""" + formatted_msg = ' '.join(line.strip() for line in detailed_msg.splitlines()) + self.msg = formatted_msg + self.log(formatted_msg, "INFO") + return self + + payload_params = { + "deviceUuids": device_uuids, + "password": password, + "operationEnum": export_device_list.get("operation_enum", "0"), + "parameters": export_device_list.get("parameters") + } + + response = self.trigger_export_api(payload_params) + self.check_return_status() + + if payload_params["operationEnum"] == "0": + temp_file_name = response.filename + output_file_name = temp_file_name.split(".")[0] + ".csv" + csv_reader = self.decrypt_and_read_csv(response, password) + self.check_return_status() + else: + decoded_resp = response.data.decode(encoding='utf-8') + self.log("Decoded response of Export Device Credential file: {0}".format(str(decoded_resp)), "DEBUG") + + # Parse the CSV-like string into a list of dictionaries + csv_reader = csv.DictReader(StringIO(decoded_resp)) + current_date = datetime.now() + formatted_date = current_date.strftime("%m-%d-%Y") + output_file_name = "devices-" + str(formatted_date) + ".csv" + + device_data = [] + for row in csv_reader: + device_data.append(row) + + # Write the data to a CSV file + with open(output_file_name, 'w', newline='') as csv_file: + fieldnames = device_data[0].keys() + csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + csv_writer.writeheader() + csv_writer.writerows(device_data) + + self.msg = "Device Details Exported Successfully to the CSV file: {0}".format(output_file_name) + self.log(self.msg, "INFO") + self.status = "success" + self.result['changed'] = True + + except Exception as e: + self.msg = "Error while exporting device details into CSV file for device(s): '{0}'".format(str(device_ips)) + self.log(self.msg, "ERROR") + self.status = "failed" + + return self + + def get_ap_devices(self, device_ips): + """ + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The management IP address of the device for which the response is to be retrieved. + Returns: + list: A list containing Access Point device IP's obtained from the Cisco Catalyst Center. + Description: + This method communicates with Cisco Catalyst Center to retrieve the details of a device with the specified + management IP address and check if device family matched to Unified AP. It executes the 'get_device_list' + API call with the provided device IP address, logs the response, and returns list containing ap device ips. + """ + + ap_device_list = [] + for device_ip in device_ips: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response', []) + + if response and response[0].get('family', '') == "Unified AP": + ap_device_list.append(device_ip) + except Exception as e: + error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "CRITICAL") + raise Exception(error_message) + + return ap_device_list + + def resync_devices(self): + """ + Resync devices in Cisco Catalyst Center. + This function performs the Resync operation for the devices specified in the playbook. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + The function expects the following parameters in the configuration: + - "ip_address": List of device IP addresses to be resynced. + - "force_sync": (Optional) Whether to force sync the devices. Defaults to "False". + """ + + # Code for triggers the resync operation using the retrieved device IDs and force sync parameter. + device_ips = self.get_device_ips_from_config_priority() + input_device_ips = device_ips.copy() + device_in_ccc = self.device_exists_in_ccc() + + for device_ip in input_device_ips: + if device_ip not in device_in_ccc: + input_device_ips.remove(device_ip) + + ap_devices = self.get_ap_devices(input_device_ips) + self.log("AP Devices from the playbook input are: {0}".format(str(ap_devices)), "INFO") + + if ap_devices: + for ap_ip in ap_devices: + input_device_ips.remove(ap_ip) + self.log("Following devices {0} are AP, so can't perform resync operation.".format(str(ap_devices)), "WARNING") + + if not input_device_ips: + self.msg = "Cannot perform the Resync operation as the device(s) with IP(s) {0} are not present in Cisco Catalyst Center".format(str(device_ips)) + self.status = "success" + self.result['changed'] = False + self.result['response'] = self.msg + self.log(self.msg, "WARNING") + return self + + device_ids = self.get_device_ids(input_device_ips) + try: + force_sync = self.config[0].get("force_sync", False) + resync_param_dict = { + 'payload': device_ids, + 'force_sync': force_sync + } + response = self.dnac._exec( + family="devices", + function='sync_devices_using_forcesync', + op_modifies=True, + params=resync_param_dict, + ) + self.log("Received API response from 'sync_devices_using_forcesync': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'Synced' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Devices have been successfully resynced. Devices resynced: {0}".format(str(input_device_ips)) + self.log(self.msg, "INFO") + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device resynced get failed because of {0}".format(failure_reason) + else: + self.msg = "Device resynced get failed." + self.log(self.msg, "ERROR") + break + + except Exception as e: + self.status = "failed" + error_message = "Error while resyncing device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + + return self + + def reboot_access_points(self): + """ + Reboot access points in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of the class with updated result, status, and log. + Description: + This function performs a reboot operation on access points in Cisco Catalyst Center based on the provided IP addresses + in the configuration. It retrieves the AP devices' MAC addresses, calls the reboot access points API, and monitors + the progress of the reboot operation. + """ + + device_ips = self.get_device_ips_from_config_priority() + input_device_ips = device_ips.copy() + + if input_device_ips: + ap_devices = self.get_ap_devices(input_device_ips) + self.log("AP Devices from the playbook input are: {0}".format(str(ap_devices)), "INFO") + for device_ip in input_device_ips: + if device_ip not in ap_devices: + input_device_ips.remove(device_ip) + + if not input_device_ips: + self.msg = "No AP Devices IP given in the playbook so can't perform reboot operation" + self.status = "success" + self.result['changed'] = False + self.result['response'] = self.msg + self.log(self.msg, "WARNING") + return self + + # Get and store the apEthernetMacAddress of given devices + ap_mac_address_list = [] + for device_ip in input_device_ips: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response') + if not response: + continue + + response = response[0] + ap_mac_address = response.get('apEthernetMacAddress') + + if ap_mac_address is not None: + ap_mac_address_list.append(ap_mac_address) + + if not ap_mac_address_list: + self.status = "success" + self.result['changed'] = False + self.msg = "Cannot find the AP devices for rebooting" + self.result['response'] = self.msg + self.log(self.msg, "INFO") + return self + + # Now call the Reboot Access Point API + reboot_params = { + "apMacAddresses": ap_mac_address_list + } + response = self.dnac._exec( + family="wireless", + function='reboot_access_points', + op_modifies=True, + params=reboot_params, + ) + self.log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'url' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "AP Device(s) {0} successfully rebooted!".format(str(input_device_ips)) + self.log(self.msg, "INFO") + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "AP Device Rebooting get failed because of {0}".format(failure_reason) + else: + self.msg = "AP Device Rebooting get failed" + self.log(self.msg, "ERROR") + break + + return self + + def handle_successful_provisioning(self, device_ip, execution_details, device_type): + """ + Handle successful provisioning of Wired/Wireless device. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - device_ip (str): The IP address of the provisioned device. + - execution_details (str): Details of the provisioning execution. + - device_type (str): The type or category of the provisioned device(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs the successful provisioning of a device. + """ + + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.log("{0} Device {1} provisioned successfully!!".format(device_type, device_ip), "INFO") + + def handle_failed_provisioning(self, device_ip, execution_details, device_type): + """ + Handle failed provisioning of Wired/Wireless device. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - device_ip (str): The IP address of the device that failed provisioning. + - execution_details (dict): Details of the failed provisioning execution in key "failureReason" indicating reason for failure. + - device_type (str): The type or category of the provisioned device(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs the failure of provisioning for a device. + """ + + self.status = "failed" + failure_reason = execution_details.get("failureReason", "Unknown failure reason") + self.msg = "{0} Device Provisioning failed for {1} because of {2}".format(device_type, device_ip, failure_reason) + self.log(self.msg, "WARNING") + + def handle_provisioning_exception(self, device_ip, exception, device_type): + """ + Handle an exception during the provisioning process of Wired/Wireless device.. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - device_ip (str): The IP address of the device involved in provisioning. + - exception (Exception): The exception raised during provisioning. + - device_type (str): The type or category of the provisioned device(Wired/Wireless). + Return: + None + Description: + This method logs an error message indicating an exception occurred during the provisioning process for a device. + """ + + error_message = "Error while Provisioning the {0} device {1} in Cisco Catalyst Center: {2}".format(device_type, device_ip, str(exception)) + self.log(error_message, "ERROR") + + def handle_all_already_provisioned(self, device_ips, device_type): + """ + Handle successful provisioning for all devices(Wired/Wireless). + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - device_type (str): The type or category of the provisioned device(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs the successful provisioning for all devices(Wired/Wireless). + """ + + self.status = "success" + self.msg = "All the {0} Devices '{1}' given in the playbook are already Provisioned".format(device_type, str(device_ips)) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + self.result['changed'] = False + + def handle_all_provisioned(self, device_type): + """ + Handle successful provisioning for all devices(Wired/Wireless). + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - device_type (str): The type or category of the provisioned devices(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs the successful provisioning for all devices(Wired/Wireless). + """ + + self.status = "success" + self.result['changed'] = True + self.log("All {0} Devices provisioned successfully!!".format(device_type), "INFO") + + def handle_all_failed_provision(self, device_type): + """ + Handle failure of provisioning for all devices(Wired/Wireless). + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - device_type (str): The type or category of the devices(Wired/Wireless). + Return: + None + Description: + This method updates the status and logs a failure message indicating that + provisioning failed for all devices of a specific type. + """ + + self.status = "failed" + self.msg = "{0} Device Provisioning failed for all devices".format(device_type) + self.log(self.msg, "INFO") + + def handle_partially_provisioned(self, provision_count, device_type): + """ + Handle partial success in provisioning for devices(Wired/Wireless). + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - provision_count (int): The count of devices that were successfully provisioned. + - device_type (str): The type or category of the provisioned devices(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs a partial success message indicating that provisioning was successful + for a certain number of devices(Wired/Wireless). + """ + + self.status = "success" + self.result['changed'] = True + self.log("{0} Devices provisioned successfully partially for {1} devices".format(device_type, provision_count), "INFO") + + def provisioned_wired_device(self): + """ + Provision wired devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of the class with updated result, status, and log. + Description: + This function provisions wired devices in Cisco Catalyst Center based on the configuration provided. + It retrieves the site name and IP addresses of the devices from the configuration, + attempts to provision each device, and monitors the provisioning process. + """ + + site_name = self.config[0]['provision_wired_device']['site_name'] + device_in_ccc = self.device_exists_in_ccc() + device_ips = self.get_device_ips_from_config_priority() + input_device_ips = device_ips.copy() + + for device_ip in input_device_ips: + if device_ip not in device_in_ccc: + input_device_ips.remove(device_ip) + + device_type = "Wired" + provision_count, already_provision_count = 0, 0 + + if not site_name and not input_device_ips: + self.status = "failed" + self.msg = "Site/Devices are required for Provisioning of Wired Devices." + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + provision_wired_params = { + 'siteNameHierarchy': site_name + } + + for device_ip in input_device_ips: + try: + provision_wired_params['deviceManagementIpAddress'] = device_ip + count = 1 + managed_flag = True + + # Check till device comes into managed state + while True: + response = self.get_device_response(device_ip) + self.log("Device is in {0} state waiting for Managed State.".format(response['managementState']), "DEBUG") + + if ( + response.get('managementState') == "Managed" + and response.get('collectionStatus') == "Managed" + and response.get("hostname") + ): + break + count = count + 1 + if count > 400: + managed_flag = False + break + + if not managed_flag: + self.log("Device {0} is not transitioning to the managed state, so provisioning operation cannot be performed." + .format(device_ip), "WARNING") + continue + + response = self.dnac._exec( + family="sda", + function='provision_wired_device', + op_modifies=True, + params=provision_wired_params, + ) + + if response.get("status") == "failed": + description = response.get("description") + error_msg = "Cannot do Provisioning for device {0} beacuse of {1}".format(device_ip, description) + self.log(error_msg) + continue + + task_id = response.get("taskId") + + while True: + execution_details = self.get_task_details(task_id) + progress = execution_details.get("progress") + self.log(progress) + + if 'TASK_PROVISION' in progress: + self.handle_successful_provisioning(device_ip, execution_details, device_type) + provision_count += 1 + break + elif execution_details.get("isError"): + self.handle_failed_provisioning(device_ip, execution_details, device_type) + break + + except Exception as e: + # Not returning from here as there might be possiblity that for some devices it comes into exception + # but for others it gets provision successfully or If some devices are already provsioned + self.handle_provisioning_exception(device_ip, e, device_type) + if "already provisioned" in str(e): + self.log(str(e), "INFO") + already_provision_count += 1 + + # Check If all the devices are already provsioned, return from here only + if already_provision_count == len(device_ips): + self.handle_all_already_provisioned(device_ips, device_type) + elif provision_count == len(device_ips): + self.handle_all_provisioned(device_type) + elif provision_count == 0: + self.handle_all_failed_provision(device_type) + else: + self.handle_partially_provisioned(provision_count, device_type) + + return self + + def get_wireless_param(self, device_ip): + """ + Get wireless provisioning parameters for a device. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The IP address of the device for which to retrieve wireless provisioning parameters. + Returns: + wireless_param (list of dict): A list containing a dictionary with wireless provisioning parameters. + Description: + This function constructs a list containing a dictionary with wireless provisioning parameters based on the + configuration provided in the playbook. It validates the managed AP locations, ensuring they are of type "floor." + The function then queries Cisco Catalyst Center to get network device details using the provided device IP. + If the device is not found, the function returns the class instance with appropriate status and log messages and + returns the wireless provisioning parameters containing site information, managed AP + locations, dynamic interfaces, and device name. + """ + + wireless_config = self.config[0]['provision_wireless_device'][0] + wireless_param = [ + { + 'site': wireless_config['site_name'], + 'managedAPLocations': wireless_config['managed_ap_locations'], + } + ] + + for ap_loc in wireless_param[0]["managedAPLocations"]: + if self.get_site_type(site_name=ap_loc) != "floor": + self.status = "failed" + self.msg = "Managed AP Location must be a floor" + self.log(self.msg, "ERROR") + return self + + wireless_param[0]["dynamicInterfaces"] = [] + + for interface in wireless_config.get("dynamic_interfaces"): + interface_dict = { + "interfaceIPAddress": interface.get("interface_ip_address"), + "interfaceNetmaskInCIDR": interface.get("interface_netmask_in_cidr"), + "interfaceGateway": interface.get("interface_gateway"), + "lagOrPortNumber": interface.get("lag_or_port_number"), + "vlanId": interface.get("vlan_id"), + "interfaceName": interface.get("interface_name") + } + wireless_param[0]["dynamicInterfaces"].append(interface_dict) + + response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"ip_address": device_ip} + ) + if not response: + self.status = "failed" + self.msg = "Device Host name is not present in the Cisco Catalyst Center" + self.log(self.msg, "INFO") + return self + + response = response.get("response") + wireless_param[0]["deviceName"] = response.get("hostname") + self.wireless_param = wireless_param + self.status = "success" + self.log("Successfully collected all parameters required for Wireless Provisioing", "DEBUG") + + return self + + def get_site_type(self, site_name): + """ + Get the type of a site in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + site_name (str): The name of the site for which to retrieve the type. + Returns: + site_type (str or None): The type of the specified site, or None if the site is not found. + Description: + This function queries Cisco Catalyst Center to retrieve the type of a specified site. It uses the + get_site API with the provided site name, extracts the site type from the response, and returns it. + If the specified site is not found, the function returns None, and an appropriate log message is generated. + """ + + try: + site_type = None + response = self.dnac_apply['exec']( + family="sites", + function='get_site', + params={"name": site_name}, + ) + + if not response: + self.msg = "Site '{0}' not found".format(site_name) + self.log(self.msg, "INFO") + return site_type + + self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") + site = response.get("response") + site_additional_info = site[0].get("additionalInfo") + + for item in site_additional_info: + if item["nameSpace"] == "Location": + site_type = item.get("attributes").get("type") + + except Exception as e: + self.msg = "Error while fetching the site '{0}'.".format(site_name) + self.log(self.msg, "ERROR") + self.module.fail_json(msg="Site not found", response=[]) + + return site_type + + def provisioned_wireless_devices(self, device_ips): + """ + Provision Wireless devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ips (list): List of IP addresses of the devices to be provisioned. + Returns: + self (object): An instance of the class with updated result, status, and log. + Description: + This function performs wireless provisioning for the provided list of device IP addresses. + It iterates through each device, retrieves provisioning parameters using the get_wireless_param function, + and then calls the Cisco Catalyst Center API for wireless provisioning. If all devices are already provisioned, + it returns success with a relevant message. + """ + + provision_count, already_provision_count = 0, 0 + device_type = "Wireless" + + device_in_ccc = self.device_exists_in_ccc() + device_ips = self.get_device_ips_from_config_priority() + input_device_ips = device_ips.copy() + + for device_ip in input_device_ips: + if device_ip not in device_in_ccc: + input_device_ips.remove(device_ip) + + for device_ip in input_device_ips: + try: + # Collect the device parameters from the playbook to perform wireless provisioing + self.get_wireless_param(device_ip).check_return_status() + provisioning_params = self.wireless_param + count = 1 + managed_flag = True + + # Check till device comes into managed state + while True: + response = self.get_device_response(device_ip) + self.log("Device is in {0} state waiting for Managed State.".format(response['managementState']), "DEBUG") + + if ( + response.get('managementState') == "Managed" + and response.get('collectionStatus') == "Managed" + and response.get("hostname") + ): + break + + count = count + 1 + if count > 200: + managed_flag = False + break + + if not managed_flag: + self.log("Device {0} is not transitioning to the managed state, so provisioning operation cannot be performed." + .format(device_ip), 'WARNING') + continue + + # Now we have provisioning_param so we can do wireless provisioning + response = self.dnac_apply['exec']( + family="wireless", + function="provision", + op_modifies=True, + params=provisioning_params, + ) + + if response.get("status") == "failed": + description = response.get("description") + error_msg = "Cannot do Provisioning for Wireless device {0} beacuse of {1}".format(device_ip, description) + self.log(error_msg, "ERROR") + continue + + task_id = response.get("taskId") + + while True: + execution_details = self.get_task_details(task_id) + progress = execution_details.get("progress") + self.log(progress) + if 'TASK_PROVISION' in progress: + self.handle_successful_provisioning(device_ip, execution_details, device_type) + provision_count += 1 + break + elif execution_details.get("isError"): + self.handle_failed_provisioning(device_ip, execution_details, device_type) + break + + except Exception as e: + # Not returning from here as there might be possiblity that for some devices it comes into exception + # but for others it gets provision successfully or If some devices are already provsioned + self.handle_provisioning_exception(device_ip, e, device_type) + if "already provisioned" in str(e): + self.msg = "Device '{0}' already provisioned".format(device_ip) + self.log(self.msg, "INFO") + already_provision_count += 1 + + # Check If all the devices are already provsioned, return from here only + if already_provision_count == len(device_ips): + self.handle_all_already_provisioned(device_ips, device_type) + elif provision_count == len(device_ips): + self.handle_all_provisioned(device_type) + elif provision_count == 0: + self.handle_all_failed_provision(device_type) + else: + self.handle_partially_provisioned(provision_count, device_type) + + return self + + def get_udf_id(self, field_name): + """ + Get the ID of a Global User Defined Field in Cisco Catalyst Center based on its name. + Parameters: + self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. + field_name (str): The name of the Global User Defined Field. + Returns: + str: The ID of the Global User Defined Field. + Description: + The function sends a request to Cisco Catalyst Center to retrieve all Global User Defined Fields + with the specified name and extracts the ID of the first matching field.If successful, it returns + the ID else returns None. + """ + + try: + response = self.dnac._exec( + family="devices", + function='get_all_user_defined_fields', + params={"name": field_name}, + ) + self.log("Received API response from 'get_all_user_defined_fields': {0}".format(str(response)), "DEBUG") + udf = response.get("response") + udf_id = udf[0].get("id") + + except Exception as e: + error_message = "Exception occurred while getting Global User Defined Fields(UDF) ID from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + + return udf_id + + def mandatory_parameter(self): + """ + Check for and validate mandatory parameters for adding network devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. + Returns: + dict: The input `config` dictionary if all mandatory parameters are present. + Description: + It will check the mandatory parameters for adding the devices in Cisco Catalyst Center. + """ + + device_type = self.config[0].get("type", "NETWORK_DEVICE") + params_dict = { + "NETWORK_DEVICE": ["enable_password", "ip_address", "password", "snmp_username", "username"], + "COMPUTE_DEVICE": ["ip_address", "http_username", "http_password", "http_port", "snmp_username"], + "MERAKI_DASHBOARD": ["http_password"], + "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address", "http_username", "http_password"], + "THIRD_PARTY_DEVICE": ["ip_address", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"] + } + + params_list = params_dict.get(device_type, []) + + mandatory_params_absent = [] + for param in params_list: + if param not in self.config[0]: + mandatory_params_absent.append(param) + + if mandatory_params_absent: + self.status = "failed" + self.msg = "Required parameters {0} for adding devices are not present".format(str(mandatory_params_absent)) + self.result['msg'] = self.msg + self.log(self.msg, "ERROR") + else: + self.status = "success" + self.msg = "Required parameter for Adding the devices in Inventory are present." + self.log(self.msg, "INFO") + + return self + + def get_have(self, config): + """ + Retrieve and check device information with Cisco Catalyst Center to determine if devices already exist. + Parameters: + self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. + config (dict): A dictionary containing the configuration details of devices to be checked. + Returns: + dict: A dictionary containing information about the devices in the playbook, devices that exist in + Cisco Catalyst Center, and devices that are not present in Cisco Catalyst Center. + Description: + This function checks the specified devices in the playbook against the devices existing in Cisco Catalyst Center with following keys: + - "want_device": A list of devices specified in the playbook. + - "device_in_ccc": A list of devices that already exist in Cisco Catalyst Center. + - "device_not_in_ccc": A list of devices that are not present in Cisco Catalyst Center. + """ + + have = {} + want_device = self.get_device_ips_from_config_priority() + + # Get the list of device that are present in Cisco Catalyst Center + device_in_ccc = self.device_exists_in_ccc() + device_not_in_ccc = [] + + for ip in want_device: + if ip not in device_in_ccc: + device_not_in_ccc.append(ip) + + self.log("Device(s) {0} exists in Cisco Catalyst Center".format(str(device_in_ccc)), "INFO") + have["want_device"] = want_device + have["device_in_ccc"] = device_in_ccc + have["device_not_in_ccc"] = device_not_in_ccc + + self.have = have + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + + return self + + def get_device_params(self, params): + """ + Extract and store device parameters from the playbook for device processing in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + params (dict): A dictionary containing device parameters retrieved from the playbook. + Returns: + dict: A dictionary containing the extracted device parameters. + Description: + This function will extract and store parameters in dictionary for adding, updating, editing, or deleting devices Cisco Catalyst Center. + """ + + device_param = { + "cliTransport": params.get("cli_transport"), + "enablePassword": params.get("enable_password"), + "password": params.get("password"), + "ipAddress": params.get("ip_address"), + "snmpAuthPassphrase": params.get("snmp_auth_passphrase"), + "snmpAuthProtocol": params.get("snmp_auth_protocol"), + "snmpMode": params.get("snmp_mode"), + "snmpPrivPassphrase": params.get("snmp_priv_passphrase"), + "snmpPrivProtocol": params.get("snmp_priv_protocol"), + "snmpROCommunity": params.get("snmp_ro_community"), + "snmpRWCommunity": params.get("snmp_rw_community"), + "snmpRetry": params.get("snmp_retry"), + "snmpTimeout": params.get("snmp_timeout"), + "snmpUserName": params.get("snmp_username"), + "userName": params.get("username"), + "computeDevice": params.get("compute_device"), + "extendedDiscoveryInfo": params.get("extended_discovery_info"), + "httpPassword": params.get("http_password"), + "httpPort": params.get("http_port"), + "httpSecure": params.get("http_secure"), + "httpUserName": params.get("http_username"), + "netconfPort": params.get("netconf_port"), + "serialNumber": params.get("serial_number"), + "snmpVersion": params.get("snmp_version"), + "type": params.get("type"), + "updateMgmtIPaddressList": params.get("update_mgmt_ipaddresslist"), + "forceSync": params.get("force_sync"), + "cleanConfig": params.get("clean_config") + } + + if device_param.get("updateMgmtIPaddressList"): + device_mngmt_dict = device_param.get("updateMgmtIPaddressList")[0] + device_param["updateMgmtIPaddressList"][0] = {} + + device_param["updateMgmtIPaddressList"][0].update( + { + "existMgmtIpAddress": device_mngmt_dict.get("exist_mgmt_ipaddress"), + "newMgmtIpAddress": device_mngmt_dict.get("new_mgmt_ipaddress") + }) + + return device_param + + def get_device_ids(self, device_ips): + """ + Get the list of unique device IDs for list of specified management IP addresses of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ips (list): The management IP addresses of devices for which you want to retrieve the device IDs. + Returns: + list: The list of unique device IDs for the specified devices. + Description: + Queries Cisco Catalyst Center to retrieve the unique device ID associated with a device having the specified + IP address. If the device is not found in Cisco Catalyst Center, then print the log message with error severity. + """ + + device_ids = [] + + for device_ip in device_ips: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + + if response: + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + if not response: + continue + device_id = response[0]["id"] + device_ids.append(device_id) + + except Exception as e: + error_message = "Error while fetching device '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)) + self.log(error_message, "ERROR") + + return device_ids + + def get_device_ips_from_hostname(self, hostname_list): + """ + Get the list of unique device IPs for list of specified hostnames of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + hostname_list (list): The hostnames of devices for which you want to retrieve the device IPs. + Returns: + list: The list of unique device IPs for the specified devices hostname list. + Description: + Queries Cisco Catalyst Center to retrieve the unique device IP's associated with a device having the specified + list of hostnames. If a device is not found in Cisco Catalyst Center, an error log message is printed. + """ + + device_ips = [] + for hostname in hostname_list: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"hostname": hostname} + ) + if response: + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ips.append(device_ip) + except Exception as e: + error_message = "Exception occurred while fetching device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + + return device_ips + + def get_device_ips_from_serial_number(self, serial_number_list): + """ + Get the list of unique device IPs for a specified list of serial numbers in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + serial_number_list (list): The list of serial number of devices for which you want to retrieve the device IPs. + Returns: + list: The list of unique device IPs for the specified devices with serial numbers. + Description: + Queries Cisco Catalyst Center to retrieve the unique device IPs associated with a device having the specified + serial numbers.If a device is not found in Cisco Catalyst Center, an error log message is printed. + """ + + device_ips = [] + for serial_number in serial_number_list: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"serialNumber": serial_number} + ) + if response: + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ips.append(device_ip) + except Exception as e: + error_message = "Exception occurred while fetching device from Cisco Catalyst Center - {0}".format(str(e)) + self.log(error_message, "ERROR") + + return device_ips + + def get_device_ips_from_mac_address(self, mac_address_list): + """ + Get the list of unique device IPs for list of specified mac address of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + mac_address_list (list): The list of mac address of devices for which you want to retrieve the device IPs. + Returns: + list: The list of unique device IPs for the specified devices. + Description: + Queries Cisco Catalyst Center to retrieve the unique device IPs associated with a device having the specified + mac addresses. If a device is not found in Cisco Catalyst Center, an error log message is printed. + """ + + device_ips = [] + for mac_address in mac_address_list: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"macAddress": mac_address} + ) + if response: + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get("response") + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ips.append(device_ip) + except Exception as e: + error_message = "Exception occurred while fetching device from Cisco Catalyst Center - {0}".format(str(e)) + self.log(error_message, "ERROR") + + return device_ips + + def get_interface_from_id_and_name(self, device_id, interface_name): + """ + Retrieve the interface ID for a device in Cisco Catalyst Center based on device id and interface name. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_id (str): The id of the device. + interface_name (str): Name of the interface for which details need to be collected. + Returns: + str: The interface ID for the specified device and interface name. + Description: + The function sends a request to Cisco Catalyst Center to retrieve the interface information + for the device with the provided device id and interface name and extracts the interface ID from the + response, and returns the interface ID. + """ + + try: + interface_detail_params = { + 'device_id': device_id, + 'name': interface_name + } + response = self.dnac._exec( + family="devices", + function='get_interface_details', + params=interface_detail_params + ) + self.log("Received API response from 'get_interface_details': {0}".format(str(response)), "DEBUG") + response = response.get("response") + + if response: + self.status = "success" + interface_id = response["id"] + self.log("""Successfully fetched interface ID ({0}) by using device id {1} and interface name {2}.""" + .format(interface_id, device_id, interface_name), "INFO") + return interface_id + + except Exception as e: + error_message = "Error while fetching interface id for interface({0}) from Cisco Catalyst Center: {1}".format(interface_name, str(e)) + self.log(error_message, "ERROR") + self.msg = error_message + self.status = "failed" + return self + + def get_interface_from_ip(self, device_ip): + """ + Get the interface ID for a device in Cisco Catalyst Center based on its IP address. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The IP address of the device. + Returns: + str: The interface ID for the specified device. + Description: + The function sends a request to Cisco Catalyst Center to retrieve the interface information + for the device with the provided IP address and extracts the interface ID from the + response, and returns the interface ID. + """ + + try: + response = self.dnac._exec( + family="devices", + function='get_interface_by_ip', + params={"ip_address": device_ip} + ) + self.log("Received API response from 'get_interface_by_ip': {0}".format(str(response)), "DEBUG") + response = response.get("response") + + if response: + interface_id = response[0]["id"] + self.log("Fetch Interface Id for device '{0}' successfully !!".format(device_ip)) + return interface_id + + except Exception as e: + error_message = "Error while fetching Interface Id for device '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + + def get_device_response(self, device_ip): + """ + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The management IP address of the device for which the response is to be retrieved. + Returns: + dict: A dictionary containing details of the device obtained from the Cisco Catalyst Center. + Description: + This method communicates with Cisco Catalyst Center to retrieve the details of a device with the specified + management IP address. It executes the 'get_device_list' API call with the provided device IP address, + logs the response, and returns a dictionary containing information about the device. + """ + + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response')[0] + + except Exception as e: + error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + + return response + + def check_device_role(self, device_ip): + """ + Checks if the device role and role source for a device in Cisco Catalyst Center match the specified values in the configuration. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The management IP address of the device for which the device role is to be checked. + Returns: + bool: True if the device role and role source match the specified values, False otherwise. + Description: + This method retrieves the device role and role source for a device in Cisco Catalyst Center using the + 'get_device_response' method and compares the retrieved values with specified values in the configuration + for updating device roles. + """ + + device_role_args = self.config[0].get('update_device_role') + role = device_role_args.get('role') + role_source = device_role_args.get('role_source') + response = self.get_device_response(device_ip) + + return response.get('role') == role and response.get('roleSource') == role_source + + def check_interface_details(self, device_ip, interface_name): + """ + Checks if the interface details for a device in Cisco Catalyst Center match the specified values in the configuration. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The management IP address of the device for which interface details are to be checked. + Returns: + bool: True if the interface details match the specified values, False otherwise. + Description: + This method retrieves the interface details for a device in Cisco Catalyst Center using the 'get_interface_by_ip' API call. + It then compares the retrieved details with the specified values in the configuration for updating interface details. + If all specified parameters match the retrieved values or are not provided in the playbook parameters, the function + returns True, indicating successful validation. + """ + device_id = self.get_device_ids([device_ip]) + + if not device_id: + self.log("""Error: Device with IP '{0}' not found in Cisco Catalyst Center.Unable to update interface details.""" + .format(device_ip), "ERROR") + return False + + interface_detail_params = { + 'device_id': device_id[0], + 'name': interface_name + } + response = self.dnac._exec( + family="devices", + function='get_interface_details', + params=interface_detail_params + ) + self.log("Received API response from 'get_interface_details': {0}".format(str(response)), "DEBUG") + response = response.get("response") + + if not response: + self.log("No response received from the API 'get_interface_details'.", "DEBUG") + return False + + response_params = { + 'description': response.get('description'), + 'adminStatus': response.get('adminStatus'), + 'voiceVlanId': response.get('voiceVlan'), + 'vlanId': int(response.get('vlanId')) + } + + interface_playbook_params = self.config[0].get('update_interface_details') + playbook_params = { + 'description': interface_playbook_params.get('description', ''), + 'adminStatus': interface_playbook_params.get('admin_status'), + 'voiceVlanId': interface_playbook_params.get('voice_vlan_id', ''), + 'vlanId': interface_playbook_params.get('vlan_id') + } + + for key, value in playbook_params.items(): + if not value: + continue + elif response_params[key] != value: + return False + + return True + + def check_credential_update(self): + """ + Checks if the credentials for devices in the configuration match the updated values in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + bool: True if the credentials match the updated values, False otherwise. + Description: + This method triggers the export API in Cisco Catalyst Center to obtain the updated credential details for + the specified devices. It then decrypts and reads the CSV file containing the updated credentials, + comparing them with the credentials specified in the configuration. + """ + + device_ips = self.get_device_ips_from_config_priority() + device_uuids = self.get_device_ids(device_ips) + password = "Testing@123" + payload_params = {"deviceUuids": device_uuids, "password": password, "operationEnum": "0"} + response = self.trigger_export_api(payload_params) + self.check_return_status() + csv_reader = self.decrypt_and_read_csv(response, password) + self.check_return_status() + device_data = next(csv_reader, None) + + if not device_data: + return False + + csv_data_dict = { + 'snmp_retry': device_data['snmp_retries'], + 'username': device_data['cli_username'], + 'password': device_data['cli_password'], + 'enable_password': device_data['cli_enable_password'], + 'snmp_username': device_data['snmpv3_user_name'], + 'snmp_auth_protocol': device_data['snmpv3_auth_type'], + } + + config = self.config[0] + for key in csv_data_dict: + if key in config and csv_data_dict[key] is not None: + if key == "snmp_retry" and int(csv_data_dict[key]) != int(config[key]): + return False + elif csv_data_dict[key] != config[key]: + return False + + return True + + def get_provision_wired_device(self, device_ip): + """ + Retrieves the provisioning status of a wired device with the specified management IP address in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The management IP address of the wired device for which provisioning status is to be retrieved. + Returns: + bool: True if the device is provisioned successfully, False otherwise. + Description: + This method communicates with Cisco Catalyst Center to check the provisioning status of a wired device. + It executes the 'get_provisioned_wired_device' API call with the provided device IP address and + logs the response. + """ + + response = self.dnac._exec( + family="sda", + function='get_provisioned_wired_device', + op_modifies=True, + params={"device_management_ip_address": device_ip} + ) + + if response.get("status") == "failed": + self.log("Cannot do provisioning for wired device {0} because of {1}.".format(device_ip, response.get('description')), "ERROR") + return False + + return True + + def update_interface_detail_of_device(self, device_to_update): + """ + Update interface details for a device in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_to_update (list): A list of IP addresses of devices to be updated. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method updates interface details for devices in Cisco Catalyst Center. + It iterates over the list of devices to be updated, retrieves interface parameters from the configuration, + calls the update interface details API with the required parameters, and checks the execution response. + If the update is successful, it sets the status to 'success' and logs an informational message. + """ + + # Call the Get interface details by device IP API and fetch the interface Id + for device_ip in device_to_update: + interface_params = self.config[0].get('update_interface_details') + interface_name = interface_params.get('interface_name') + device_id = self.get_device_ids([device_ip]) + interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) + self.check_return_status() + + # Now we call update interface details api with required parameter + try: + interface_params = self.config[0].get('update_interface_details') + temp_params = { + 'description': interface_params.get('description', ''), + 'adminStatus': interface_params.get('admin_status'), + 'voiceVlanId': interface_params.get('voice_vlan_id'), + 'vlanId': interface_params.get('vlan_id') + } + payload_params = {} + for key, value in temp_params.items(): + if value is not None: + payload_params[key] = value + + update_interface_params = { + 'payload': payload_params, + 'interface_uuid': interface_id, + 'deployment_mode': interface_params.get('deployment_mode', 'Deploy') + } + response = self.dnac._exec( + family="devices", + function='update_interface_details', + op_modifies=True, + params=update_interface_params, + ) + self.log("Received API response from 'update_interface_details': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'SUCCESS' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Updated Interface Details for device '{0}' successfully".format(device_ip) + self.log(self.msg, "INFO") + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Interface Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Interface Updation get failed" + self.log(self.msg, "ERROR") + break + + except Exception as e: + error_message = "Error while updating interface details in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "INFO") + self.status = "success" + self.result['changed'] = False + self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" + self.log(self.msg, "INFO") + + return self + + def check_managementip_execution_response(self, response, device_ip, new_mgmt_ipaddress): + """ + Check the execution response of a management IP update task. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + response (dict): The response received after initiating the management IP update task. + device_ip (str): The IP address of the device for which the management IP was updated. + new_mgmt_ipaddress (str): The new management IP address of the device. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the execution response of a management IP update task in Cisco Catalyst Center. + It continuously queries the task details until the task is completed or an error occurs. + If the task is successful, it sets the status to 'success' and logs an informational message. + If the task fails, it sets the status to 'failed' and logs an error message with the failure reason, if available. + """ + + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device new management IP updation for device '{0}' get failed due to {1}".format(device_ip, failure_reason) + else: + self.msg = "Device new management IP updation for device '{0}' get failed".format(device_ip) + self.log(self.msg, "ERROR") + break + elif execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = """Device '{0}' present in Cisco Catalyst Center and new management ip '{1}' have been + updated successfully""".format(device_ip, new_mgmt_ipaddress) + self.log(self.msg, "INFO") + break + + return self + + def check_device_update_execution_response(self, response, device_ip): + """ + Check the execution response of a device update task. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + response (dict): The response received after initiating the device update task. + device_ip (str): The IP address of the device for which the update is performed. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the execution response of a device update task in Cisco Catalyst Center. + It continuously queries the task details until the task is completed or an error occurs. + If the task is successful, it sets the status to 'success' and logs an informational message. + If the task fails, it sets the status to 'failed' and logs an error message with the failure reason, if available. + """ + + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Updation for device '{0}' get failed due to {1}".format(device_ip, failure_reason) + else: + self.msg = "Device Updation for device '{0}' get failed".format(device_ip) + self.log(self.msg, "ERROR") + break + elif execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Device '{0}' present in Cisco Catalyst Center and have been updated successfully".format(device_ip) + self.log(self.msg, "INFO") + break + + return self + + def get_want(self, config): + """ + Get all the device related information from playbook that is needed to be + add/update/delete/resync device in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): A dictionary containing device-related information from the playbook. + Returns: + dict: A dictionary containing the extracted device parameters and other relevant information. + Description: + Retrieve all the device-related information from the playbook needed for adding, updating, deleting, + or resyncing devices in Cisco Catalyst Center. + """ + + want = {} + device_params = self.get_device_params(config) + want["device_params"] = device_params + + self.want = want + self.msg = "Successfully collected all parameters from the playbook " + self.status = "success" + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + return self + + def get_diff_merged(self, config): + """ + Merge and process differences between existing devices and desired device configuration in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. + Returns: + object: An instance of the class with updated results and status based on the processing of differences. + Description: + The function processes the differences and, depending on the changes required, it may add, update, + or resynchronize devices in Cisco Catalyst Center. + The updated results and status are stored in the class instance for further use. + """ + + devices_to_add = self.have["device_not_in_ccc"] + device_type = self.config[0].get("type", "NETWORK_DEVICE") + device_resynced = self.config[0].get("device_resync", False) + device_added = self.config[0].get("device_added", False) + device_updated = self.config[0].get("device_updated", False) + device_reboot = self.config[0].get("reboot_device", False) + credential_update = self.config[0].get("credential_update", False) + + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + + if field_name is None: + self.status = "failed" + self.msg = "Error: The mandatory parameter 'name' for the User Defined Field is missing. Please provide the required information." + self.log(self.msg, "ERROR") + return self + + # Check if the Global User defined field exist if not then create it with given field name + udf_exist = self.is_udf_exist(field_name) + + if not udf_exist: + # Create the Global UDF + self.create_user_defined_field().check_return_status() + + # Get device Id based on config priority + device_ips = self.get_device_ips_from_config_priority() + device_ids = self.get_device_ids(device_ips) + + if len(device_ids) == 0: + self.status = "failed" + self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" + self.log(self.msg, "INFO") + self.result['changed'] = False + return self + + # Now add code for adding Global UDF to device with Id + self.add_field_to_devices(device_ids).check_return_status() + + self.result['changed'] = True + self.msg = "Global User Defined Field(UDF) named '{0}' has been successfully added to the device.".format(field_name) + self.log(self.msg, "INFO") + + config['type'] = device_type + if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": + config['http_port'] = self.config[0].get("http_port", "443") + + if device_updated: + device_to_update = self.get_device_ips_from_config_priority() + # First check if device present in Cisco Catalyst Center or not + device_present = False + for device in device_to_update: + if device in self.have.get("device_in_ccc"): + device_present = True + break + + if not device_present: + self.msg = "Cannot perform Update operation as device: {0} not present in Cisco Catalyst Center".format(str(device_to_update)) + self.status = "success" + self.result['changed'] = False + self.result['response'] = self.msg + self.log(self.msg, "INFO") + return self + + if credential_update: + # Update Device details and credentails + device_uuids = self.get_device_ids(device_to_update) + password = "Testing@123" + export_payload = {"deviceUuids": device_uuids, "password": password, "operationEnum": "0"} + export_response = self.trigger_export_api(export_payload) + self.check_return_status() + csv_reader = self.decrypt_and_read_csv(export_response, password) + self.check_return_status() + device_details = {} + + for row in csv_reader: + ip_address = row['ip_address'] + device_details[ip_address] = row + + for device_ip in device_to_update: + playbook_params = self.want.get("device_params").copy() + playbook_params['ipAddress'] = [device_ip] + device_data = device_details[device_ip] + + if not playbook_params['cliTransport']: + if device_data['protocol'] == "ssh2": + playbook_params['cliTransport'] = "ssh" + else: + playbook_params['cliTransport'] = device_data['protocol'] + if not playbook_params['snmpPrivProtocol']: + playbook_params['snmpPrivProtocol'] = device_data['snmpv3_privacy_type'] + + csv_data_dict = { + 'username': device_data['cli_username'], + 'password': device_data['cli_password'], + 'enable_password': device_data['cli_enable_password'], + 'netconf_port': device_data['netconf_port'], + } + + if device_data['snmp_version'] == '3': + csv_data_dict['snmp_username'] = device_data['snmpv3_user_name'] + if device_data['snmpv3_privacy_password']: + csv_data_dict['snmp_auth_passphrase'] = device_data['snmpv3_auth_password'] + csv_data_dict['snmp_priv_passphrase'] = device_data['snmpv3_privacy_password'] + + device_key_mapping = { + 'username': 'userName', + 'password': 'password', + 'enable_password': 'enablePassword', + 'snmp_username': 'snmpUserName', + 'netconf_port': 'netconfPort' + } + device_update_key_list = ["username", "password", "enable_password", "snmp_username", "netconf_port"] + + for key in device_update_key_list: + mapped_key = device_key_mapping[key] + + if playbook_params[mapped_key] is None: + if playbook_params['snmpMode'] == "AUTHPRIV": + playbook_params['snmpAuthPassphrase'] = csv_data_dict['snmp_auth_passphrase'] + playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] + playbook_params[mapped_key] = csv_data_dict[key] + + if playbook_params['snmpMode'] == "NOAUTHNOPRIV": + playbook_params.pop('snmpAuthPassphrase', None) + playbook_params.pop('snmpPrivPassphrase', None) + playbook_params.pop('snmpPrivProtocol', None) + playbook_params.pop('snmpAuthProtocol', None) + elif playbook_params['snmpMode'] == "AUTHNOPRIV": + playbook_params.pop('snmpPrivPassphrase', None) + playbook_params.pop('snmpPrivProtocol', None) + + if playbook_params['netconfPort'] == " ": + playbook_params['netconfPort'] = None + + try: + if playbook_params['updateMgmtIPaddressList']: + new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] + if new_mgmt_ipaddress in self.have['device_in_ccc']: + self.status = "failed" + self.msg = "Device with IP address '{0}' already exists in inventory".format(new_mgmt_ipaddress) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + else: + self.log("Playbook parameter for updating device new management ip address: {0}".format(str(playbook_params)), "DEBUG") + response = self.dnac._exec( + family="devices", + function='sync_devices', + op_modifies=True, + params=playbook_params, + ) + self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + self.check_managementip_execution_response(response, device_ip, new_mgmt_ipaddress) + self.check_return_status() + + else: + self.log("Playbook parameter for updating devices: {0}".format(str(playbook_params)), "DEBUG") + response = self.dnac._exec( + family="devices", + function='sync_devices', + op_modifies=True, + params=playbook_params, + ) + self.log("Received API response from 'sync_devices': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + self.check_device_update_execution_response(response, device_ip) + self.check_return_status() + + except Exception as e: + error_message = "Error while updating device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + + if self.config[0].get('update_interface_details'): + self.update_interface_detail_of_device(device_to_update).check_return_status() + + if self.config[0].get('update_device_role'): + for device_ip in device_to_update: + device_id = self.get_device_ids([device_ip]) + device_role_args = self.config[0].get('update_device_role') + + if 'role' not in device_role_args or 'role_source' not in device_role_args: + self.status = "failed" + self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" + self.log(self.msg, "WARNING") + return self + + # Check if the same role of device is present in ccc then no need to change the state + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response')[0] + + if response.get('role') == device_role_args.get('role'): + self.status = "success" + self.result['changed'] = False + log_msg = "The device role '{0}' is already set in Cisco Catalyst Center, no update is needed.".format(device_role_args.get('role')) + self.log(log_msg, "INFO") + continue + + device_role_params = { + 'role': device_role_args.get('role'), + 'roleSource': device_role_args.get('role_source'), + 'id': device_id[0] + } + + try: + response = self.dnac._exec( + family="devices", + function='update_device_role', + op_modifies=True, + params=device_role_params, + ) + self.log("Received API response from 'update_device_role': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'successfully' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Device(s) '{0}' role updated successfully".format(str(device_to_update)) + self.log(self.msg, "INFO") + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device role updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Device role updation get failed" + self.log(self.msg, "ERROR") + break + + except Exception as e: + error_message = "Error while updating device role in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + + # If we want to add device in inventory + if device_added: + config['ip_address'] = devices_to_add + device_params = self.want.get("device_params") + + if not device_params['cliTransport']: + device_params['cliTransport'] = "ssh" + + if not device_params['snmpPrivProtocol']: + device_params['snmpPrivProtocol'] = "AES128" + + if device_params['snmpMode'] == "NOAUTHNOPRIV": + device_params.pop('snmpAuthPassphrase', None) + device_params.pop('snmpPrivPassphrase', None) + device_params.pop('snmpPrivProtocol', None) + device_params.pop('snmpAuthProtocol', None) + elif device_params['snmpMode'] == "AUTHNOPRIV": + device_params.pop('snmpPrivPassphrase', None) + device_params.pop('snmpPrivProtocol', None) + + self.mandatory_parameter().check_return_status() + try: + response = self.dnac._exec( + family="devices", + function='add_device', + op_modifies=True, + params=device_params, + ) + self.log("Received API response from 'add_device': {0}".format(str(response)), "DEBUG") + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if '/task/' in execution_details.get("progress"): + self.status = "success" + self.result['response'] = execution_details + + if len(devices_to_add) > 0: + self.result['changed'] = True + self.msg = "Device(s) '{0}' added to Cisco Catalyst Center".format(str(devices_to_add)) + self.log(self.msg, "INFO") + self.result['msg'] = self.msg + break + self.msg = "Device(s) '{0}' already present in Cisco Catalyst Center".format(str(self.config[0].get("ip_address"))) + self.log(self.msg, "INFO") + self.result['msg'] = self.msg + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device addition get failed because of {0}".format(failure_reason) + else: + self.msg = "Device addition get failed" + self.log(self.msg, "ERROR") + self.result['msg'] = self.msg + break + + except Exception as e: + error_message = "Error while adding device in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + + if field_name is None: + self.status = "failed" + self.msg = "Mandatory paramter for User Define Field 'name' is missing" + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + # Check if the Global User defined field exist if not then create it with given field name + udf_exist = self.is_udf_exist(field_name) + + if not udf_exist: + # Create the Global UDF + self.create_user_defined_field().check_return_status() + + # Get device Id based on config priority + device_ips = self.get_device_ips_from_config_priority() + device_ids = self.get_device_ids(device_ips) + + if not device_ids: + self.status = "failed" + self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" + self.result['changed'] = False + self.result['response'] = self.msg + self.log(self.msg, "INFO") + return self + + # Now add code for adding Global UDF to device with Id + self.add_field_to_devices(device_ids).check_return_status() + + self.result['changed'] = True + self.msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) + self.log(self.msg, "INFO") + + # Once Wired device get added we will assign device to site and Provisioned it + if self.config[0].get('provision_wired_device'): + self.provisioned_wired_device().check_return_status() + + # Once Wireless device get added we will assign device to site and Provisioned it + if self.config[0].get('provision_wireless_device'): + device_ips = self.get_device_ips_from_config_priority() + self.provisioned_wireless_devices(device_ips).check_return_status() + + if device_resynced: + self.resync_devices().check_return_status() + + if device_reboot: + self.reboot_access_points().check_return_status() + + if self.config[0].get('export_device_list'): + self.export_device_details().check_return_status() + + return self + + def get_diff_deleted(self, config): + """ + Delete devices in Cisco Catalyst Center based on device IP Address. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center + config (dict): A dictionary containing the list of device IP addresses to be deleted. + Returns: + object: An instance of the class with updated results and status based on the deletion operation. + Description: + This function is responsible for removing devices from the Cisco Catalyst Center inventory and + also unprovsioned and removed wired provsion devices from the Inventory page and also delete + the Global User Defined Field that are associated to the devices. + """ + + device_to_delete = self.get_device_ips_from_config_priority() + self.result['msg'] = [] + + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + udf_id = self.get_udf_id(field_name) + + if udf_id is None: + self.status = "success" + self.msg = "Global UDF '{0}' is not present in Cisco Catalyst Center".format(field_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['msg'] = self.msg + return self + + try: + response = self.dnac._exec( + family="devices", + function='delete_user_defined_field', + params={"id": udf_id}, + ) + if response and isinstance(response, dict): + self.log("Received API response from 'delete_user_defined_field': {0}".format(str(response)), "DEBUG") + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'success' in execution_details.get("progress"): + self.status = "success" + self.msg = "Global UDF '{0}' deleted successfully from Cisco Catalyst Center".format(field_name) + self.log(self.msg, "INFO") + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Failed to delete Global User Defined Field(UDF) due to: {0}".format(failure_reason) + else: + self.msg = "Global UDF deletion get failed." + self.log(self.msg, "ERROR") + break + + except Exception as e: + error_message = "Error while deleting Global UDF from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + + return self + + for device_ip in device_to_delete: + if device_ip not in self.have.get("device_in_ccc"): + self.status = "success" + self.result['changed'] = False + self.msg = "Device '{0}' is not present in Cisco Catalyst Center so can't perform delete operation".format(device_ip) + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + continue + + try: + provision_params = { + "device_management_ip_address": device_ip + } + prov_respone = self.dnac._exec( + family="sda", + function='get_provisioned_wired_device', + params=provision_params, + ) + + if prov_respone.get("status") == "success": + response = self.dnac._exec( + family="sda", + function='delete_provisioned_wired_device', + params=provision_params, + ) + executionid = response.get("executionId") + + while True: + execution_details = self.get_execution_details(executionid) + if execution_details.get("status") == "SUCCESS": + self.result['changed'] = True + self.msg = execution_details.get("bapiName") + self.log(self.msg, "INFO") + self.result['response'] = self.msg + break + elif execution_details.get("bapiError"): + self.msg = execution_details.get("bapiError") + self.log(self.msg, "ERROR") + break + except Exception as e: + device_id = self.get_device_ids([device_ip]) + delete_params = { + "id": device_id[0], + "clean_config": self.config[0].get("clean_config", False) + } + response = self.dnac._exec( + family="devices", + function='delete_device_by_id', + params=delete_params, + ) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'success' in execution_details.get("progress"): + self.status = "success" + self.msg = "Device '{0}' was successfully deleted from Cisco Catalyst Center".format(device_ip) + self.log(self.msg, "INFO") + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device '{0}' deletion get failed due to: {1}".format(device_ip, failure_reason) + else: + self.msg = "Device '{0}' deletion get failed.".format(device_ip) + self.log(self.msg, "ERROR") + break + self.result['msg'] = self.msg + + return self + + def verify_diff_merged(self, config): + """ + Verify the merged status(Addition/Updation) of Devices in Cisco Catalyst Center. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the merged status of a configuration in Cisco Catalyst Center by retrieving the current state + (have) and desired state (want) of the configuration, logs the states, and validates whether the specified + site exists in the Catalyst Center configuration. + + The function performs the following verifications: + - Checks for devices added to Cisco Catalyst Center and logs the status. + - Verifies updated device roles and logs the status. + - Verifies updated interface details and logs the status. + - Verifies updated device credentials and logs the status. + - Verifies the creation of a global User Defined Field (UDF) and logs the status. + - Verifies the provisioning of wired devices and logs the status. + """ + + self.get_have(config) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + devices_to_add = self.have["device_not_in_ccc"] + device_added = self.config[0].get("device_added", False) + device_updated = self.config[0].get("device_updated", False) + credential_update = self.config[0].get("credential_update", False) + device_type = self.config[0].get("type", "NETWORK_DEVICE") + device_ips = self.get_device_ips_from_config_priority() + + if device_added: + if not devices_to_add: + self.status = "success" + msg = """Requested device(s) '{0}' have been successfully added to the Cisco Catalyst Center and their + addition has been verified.""".format(str(device_ips)) + self.log(msg, "INFO") + else: + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that the device addition + task may not have executed successfully.""", "INFO") + + if device_updated and self.config[0].get('update_interface_details'): + interface_update_flag = True + interface_name = self.config[0].get('update_interface_details').get('interface_name') + + for device_ip in device_ips: + if not self.check_interface_details(device_ip, interface_name): + interface_update_flag = False + break + + if interface_update_flag: + self.status = "success" + msg = "Interface details updated and verified successfully for devices {0}.".format(device_ips) + self.log(msg, "INFO") + else: + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that the update + interface details task may not have executed successfully.""", "INFO") + + if device_updated and credential_update and device_type == "NETWORK_DEVICE": + credential_update_flag = self.check_credential_update() + + if credential_update_flag: + self.status = "success" + msg = "Device credentials and details updated and verified successfully in Cisco Catalyst Center." + self.log(msg, "INFO") + else: + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device updation task not executed properly.", "INFO") + elif device_type != "NETWORK_DEVICE": + self.log("""Unable to compare the parameter for device type '{0}' in the playbook with the one in Cisco Catalyst Center.""" + .format(device_type), "WARNING") + + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + udf_exist = self.is_udf_exist(field_name) + + if udf_exist: + self.status = "success" + msg = "Global UDF {0} created and verified successfully".format(field_name) + self.log(msg, "INFO") + else: + self.log("""Mismatch between playbook parameter and Cisco Catalyst Center detected, indicating that + the task of creating Global UDF may not have executed successfully.""", "INFO") + + if device_updated and self.config[0].get('update_device_role'): + device_role_flag = True + + for device_ip in device_ips: + if not self.check_device_role(device_ip): + device_role_flag = False + break + + if device_role_flag: + self.status = "success" + msg = "Device roles updated and verified successfully." + self.log(msg, "INFO") + else: + self.log("""Mismatch between playbook parameter 'role' and Cisco Catalyst Center detected, indicating the + device role update task may not have executed successfully.""", "INFO") + + if self.config[0].get('provision_wired_device'): + provision_wired_flag = True + + for device_ip in device_ips: + if not self.get_provision_wired_device(device_ip): + provision_wired_flag = False + break + + if provision_wired_flag: + self.status = "success" + msg = "Wired devices {0} get provisioned and verified successfully.".format(device_ips) + self.log(msg, "INFO") + else: + self.log("""Mismatch between playbook's input and Cisco Catalyst Center detected, indicating that + the provisioning task may not have executed successfully.""", "INFO") + + return self + + def verify_diff_deleted(self, config): + """ + Verify the deletion status of Device and Global UDF in Cisco Catalyst Center. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified Devices or Global UDF deleted from Cisco Catalyst Center. + """ + + self.get_have(config) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + input_devices = self.have["want_device"] + device_in_ccc = self.device_exists_in_ccc() + + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + udf_id = self.get_udf_id(field_name) + + if udf_id is None: + self.status = "success" + msg = "Global UDF named '{0}' has been successfully deleted from Cisco Catalyst Center and the deletion has been verified.".format(field_name) + self.log(msg, "INFO") + return self + + device_delete_flag = True + for device_ip in input_devices: + if device_ip in device_in_ccc: + device_after_deletion = device_ip + device_delete_flag = False + break + + if device_delete_flag: + self.status = "success" + self.msg = "Requested device(s) '{0}' deleted from Cisco Catalyst Center and the deletion has been verified.".format(str(input_devices)) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between playbook parameter device({0}) and Cisco Catalyst Center detected, indicating that + the device deletion task may not have executed successfully.""".format(device_after_deletion), "INFO") + + return self + + +def main(): + """ main entry point for module execution + """ + + element_spec = {'dnac_host': {'type': 'str', 'required': True, }, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {'type': 'bool', "default": False}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } + + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + + ccc_device = Inventory(module) + state = ccc_device.params.get("state") + + if state not in ccc_device.supported_states: + ccc_device.status = "invalid" + ccc_device.msg = "State {0} is invalid".format(state) + ccc_device.check_return_status() + + ccc_device.validate_input().check_return_status() + config_verify = ccc_device.params.get("config_verify") + + for config in ccc_device.validated_config: + ccc_device.reset_values() + ccc_device.get_want(config).check_return_status() + ccc_device.get_have(config).check_return_status() + ccc_device.get_diff_state_apply[state](config).check_return_status() + if config_verify: + ccc_device.verify_diff_state_apply[state](config).check_return_status() + + module.exit_json(**ccc_device.result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py new file mode 100644 index 0000000000..fedd8ad4e5 --- /dev/null +++ b/plugins/modules/site_workflow_manager.py @@ -0,0 +1,1063 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ("Madhan Sankaranarayanan, Rishita Chowdhary, Abhishek Maheshwari") + +DOCUMENTATION = r""" +--- +module: site_workflow_manager +short_description: Resource module for Site operations +description: +- Manage operation create, update and delete of the resource Sites. +- Creates site with area/building/floor with specified hierarchy. +- Updates site with area/building/floor with specified hierarchy. +- Deletes site with area/building/floor with specified hierarchy. +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Madhan Sankaranarayanan (@madhansansel) + Rishita Chowdhary (@rishitachowdhary) + Abhishek Maheshwari (@abhishekmaheshwari) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. + type: bool + default: False + state: + description: The state of Catalyst Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of site being managed. + type: list + elements: dict + required: true + suboptions: + type: + description: Type of site to create/update/delete (eg area, building, floor). + type: str + site: + description: Site Details. + type: dict + suboptions: + area: + description: Site Create's area. + type: dict + suboptions: + name: + description: Name of the area (eg Area1). + type: str + parentName: + description: Complete Parent name of the Area to be created/deleted(eg Global/). + type: str + building: + description: Building Details. + type: dict + suboptions: + address: + description: Address of the building to be created. + type: str + latitude: + description: Latitude coordinate of the building (eg 37.338).Values between -90 to +90. + type: int + longitude: + description: Longitude coordinate of the building (eg -121.832).Values between -180 to +180. + type: int + name: + description: Name of the building (eg building1). + type: str + parent_name: + description: Complete Parent name of the Building to be created/deleted(eg Global/USA/San Francisco). + type: str + floor: + description: Site Create's floor. + type: dict + suboptions: + height: + description: Height of the floor units is ft. (eg 15). + type: int + length: + description: Length of the floor units is ft. (eg 100). + type: int + name: + description: Name of the floor (eg floor-1). + type: str + parentName: + description: Complete Parent name of the floor to be created(eg Global/USA/San Francisco/BGL_18). + type: str + rf_model: + description: Type of floor. Allowed values are 'Cubes And Walled Offices', + 'Drywall Office Only', 'Indoor High Ceiling', 'Outdoor Open Space'. + type: str + width: + description: Width of the floor units is ft. (eg 100). + type: int + floor_number: + description: Floor number in the building/site (eg 5).once created, it can't be modified. + type: int + +requirements: +- dnacentersdk == 2.4.5 +- python >= 3.5 +notes: + - SDK Method used are + sites.Sites.create_site, + sites.Sites.update_site, + sites.Sites.delete_site + + - Paths used are + post /dna/intent/api/v1/site, + put dna/intent/api/v1/site/{siteId}, + delete dna/intent/api/v1/site/{siteId} +""" + +EXAMPLES = r""" +- name: Create a new area site + cisco.dnac.site_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: "{{dnac_log}}" + state: merged + config: + - site: + area: + name: string + parentName: string + type: string + +- name: Create a new building site + cisco.dnac.site_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: "{{dnac_log}}" + state: merged + config: + - site: + building: + address: string + latitude: 0 + longitude: 0 + name: string + parentName: string + type: string + +- name: Create a Floor site under the building + cisco.dnac.site_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: "{{dnac_log}}" + state: merged + config: + - site: + floor: + name: string + parent_name: string + length: int + width: int + height: int + rf_model: string + floor_number: int + type: string + +- name: Updating the Floor details under the building + cisco.dnac.site_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: "{{dnac_log}}" + state: merged + config: + - site: + floor: + name: string + parent_name: string + length: int + width: int + height: int + type: string + +- name: Deleting any site you need site name and parentName + cisco.dnac.site_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: "{{dnac_log}}" + state: deleted + config: + - site: + floor: + name: string + parent_name: string + type: string +""" + +RETURN = r""" +#Case_1: Site is successfully created/updated/deleted +response_1: + description: A dictionary with API execution details as returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": + { + "bapiExecutionId": String, + "bapiKey": String, + "bapiName": String, + "endTime": String, + "endTimeEpoch": 0, + "runtimeInstanceId": String, + "siteId": String, + "startTime": String, + "startTimeEpoch": 0, + "status": String, + "timeDuration": 0 + + }, + "msg": "string" + } + +#Case_2: Site exits and does not need an update +response_2: + description: A dictionary with existing site details. + returned: always + type: dict + sample: > + { + "response": + { + "site": {}, + "siteId": String, + "type": String + }, + "msg": String + } + +#Case_3: Error while creating/updating/deleting site +response_3: + description: A dictionary with API execution details as returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": + { + "bapiError": String, + "bapiExecutionId": String, + "bapiKey": String, + "bapiName": String, + "endTime": String, + "endTimeEpoch": 0, + "runtimeInstanceId": String, + "startTime": String, + "startTimeEpoch": 0, + "status": String, + "timeDuration": 0 + + }, + "msg": "string" + } + +#Case_4: Site not found when atempting to delete site +response_4: + description: A list with the response returned by the Cisco Catalyst Center Python + returned: always + type: list + sample: > + { + "response": [], + "msg": String + } +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, + get_dict_result, +) + +floor_plan = { + '101101': 'Cubes And Walled Offices', + '101102': 'Drywall Office Only', + '101105': 'Free Space', + '101104': 'Indoor High Ceiling', + '101103': 'Outdoor Open Space' +} + + +class Site(DnacBase): + """Class containing member attributes for site intent module""" + + def __init__(self, module): + super().__init__(module) + self.supported_states = ["merged", "deleted"] + + def validate_input(self): + """ + Validate the fields provided in the playbook. + Checks the configuration provided in the playbook against a predefined specification + to ensure it adheres to the expected structure and data types. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of the 'config' parameter. + Example: + To use this method, create an instance of the class and call 'validate_input' on it. + If the validation succeeds, 'self.status' will be 'success' and 'self.validated_config' + will contain the validated configuration. If it fails, 'self.status' will be 'failed', and + 'self.msg' will describe the validation issues. + """ + + if not self.config: + self.status = "success" + self.msg = "Configuration is not available in the playbook for validation" + self.log(self.msg, "ERROR") + return self + + temp_spec = dict( + type=dict(required=False, type='str'), + site=dict(required=True, type='dict'), + ) + + # Validate site params + valid_temp, invalid_params = validate_list_of_dicts( + self.config, temp_spec + ) + + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params) + ) + self.log(self.msg, "ERROR") + self.status = "failed" + return self + + self.validated_config = valid_temp + self.msg = "Successfully validated playbook config params: {0}".format(str(valid_temp)) + self.log(self.msg, "INFO") + self.status = "success" + + return self + + def get_current_site(self, site): + """ + Get the current site information. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - site (list): A list containing information about the site. + Returns: + - dict: A dictionary containing the extracted site information. + Description: + This method extracts information about the current site based on + the provided 'site' list. It determines the type of the site + (area, building, or floor) and retrieves specific details + accordingly. The resulting dictionary includes the type, site + details, and the site ID. + """ + + site_info = {} + + location = get_dict_result(site[0].get("additionalInfo"), 'nameSpace', "Location") + typeinfo = location.get("attributes").get("type") + + if typeinfo == "area": + site_info = dict( + area=dict( + name=site[0].get("name"), + parentName=site[0].get("siteNameHierarchy").split("/" + site[0].get("name"))[0] + ) + ) + + elif typeinfo == "building": + site_info = dict( + building=dict( + name=site[0].get("name"), + parentName=site[0].get("siteNameHierarchy").split("/" + site[0].get("name"))[0], + address=location.get("attributes").get("address"), + latitude=location.get("attributes").get("latitude"), + longitude=location.get("attributes").get("longitude"), + country=location.get("attributes").get("country"), + ) + ) + + elif typeinfo == "floor": + map_geometry = get_dict_result(site[0].get("additionalInfo"), 'nameSpace', "mapGeometry") + map_summary = get_dict_result(site[0].get("additionalInfo"), 'nameSpace', "mapsSummary") + rf_model = map_summary.get("attributes").get("rfModel") + + site_info = dict( + floor=dict( + name=site[0].get("name"), + parentName=site[0].get("siteNameHierarchy").split("/" + site[0].get("name"))[0], + rf_model=floor_plan.get(rf_model), + width=map_geometry.get("attributes").get("width"), + length=map_geometry.get("attributes").get("length"), + height=map_geometry.get("attributes").get("height"), + floorNumber=map_geometry.get("attributes").get("floor_number", "") + ) + ) + + current_site = dict( + type=typeinfo, + site=site_info, + siteId=site[0].get("id") + ) + + self.log("Current site details: {0}".format(str(current_site)), "INFO") + + return current_site + + def site_exists(self): + """ + Check if the site exists in Cisco Catalyst Center. + + Parameters: + - self (object): An instance of the class containing the method. + Returns: + - tuple: A tuple containing a boolean indicating whether the site exists and + a dictionary containing information about the existing site. + The returned tuple includes two elements: + - site_exists (bool): Indicates whether the site exists. + - dict: Contains information about the existing site. If the + site doesn't exist, this dictionary is empty. + Description: + Checks the existence of a site in Cisco Catalyst Center by querying the + 'get_site' function in the 'sites' family. It utilizes the + 'site_name' parameter from the 'want' attribute to identify the site. + """ + + site_exists = False + current_site = {} + response = None + try: + response = self.dnac._exec( + family="sites", + function='get_site', + params={"name": self.want.get("site_name")}, + ) + + except Exception as e: + self.log("The provided site name '{0}' is either invalid or not present in the Cisco Catalyst Center." + .format(self.want.get("site_name")), "WARNING") + if response: + response = response.get("response") + self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") + current_site = self.get_current_site(response) + site_exists = True + self.log("Site '{0}' exists in Cisco Catalyst Center".format(self.want.get("site_name")), "INFO") + + return (site_exists, current_site) + + def get_site_params(self, params): + """ + Store the site-related parameters. + + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - params (dict): Dictionary containing site-related parameters. + Returns: + - dict: Dictionary containing the stored site-related parameters. + The returned dictionary includes the following keys: + - 'type' (str): The type of the site. + - 'site' (dict): Dictionary containing site-related info. + Description: + This method takes a dictionary 'params' containing site-related + information and stores the relevant parameters based on the site + type. If the site type is 'floor', it ensures that the 'rfModel' + parameter is stored in uppercase. + """ + typeinfo = params.get("type") + site_info = {} + + if typeinfo == 'area': + area_details = params.get('site').get('area') + site_info['area'] = { + 'name': area_details.get('name'), + 'parentName': area_details.get('parent_name') + } + elif typeinfo == 'building': + building_details = params.get('site').get('building') + site_info['building'] = { + 'name': building_details.get('name'), + 'address': building_details.get('address'), + 'parentName': building_details.get('parent_name'), + 'latitude': building_details.get('latitude'), + 'longitude': building_details.get('longitude'), + 'country': building_details.get('country') + } + else: + floor_details = params.get('site').get('floor') + site_info['floor'] = { + 'name': floor_details.get('name'), + 'parentName': floor_details.get('parent_name'), + 'length': floor_details.get('length'), + 'width': floor_details.get('width'), + 'height': floor_details.get('height'), + 'floorNumber': floor_details.get('floor_number', '') + } + try: + site_info["floor"]["rfModel"] = floor_details.get("rf_model") + except Exception as e: + self.log("The attribute 'rf_model' is missing in floor '{0}'.".format(floor_details.get('name')), "WARNING") + + site_params = dict( + type=typeinfo, + site=site_info, + ) + self.log("Site parameters: {0}".format(str(site_params)), "DEBUG") + + return site_params + + def get_site_name(self, site): + """ + Get and Return the site name. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - site (dict): A dictionary containing information about the site. + Returns: + - str: The constructed site name. + Description: + This method takes a dictionary 'site' containing information about + the site and constructs the site name by combining the parent name + and site name. + """ + + site_type = site.get("type") + parent_name = site.get("site").get(site_type).get("parent_name") + name = site.get("site").get(site_type).get("name") + site_name = '/'.join([parent_name, name]) + self.log("Site name: {0}".format(site_name), "INFO") + + return site_name + + def compare_float_values(self, ele1, ele2, precision=2): + """ + Compare two floating-point values with a specified precision. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - ele1 (float): The first floating-point value to be compared. + - ele2 (float): The second floating-point value to be compared. + - precision (int, optional): The number of decimal places to consider in the comparison, Defaults to 2. + Return: + bool: True if the rounded values are equal within the specified precision, False otherwise. + Description: + This method compares two floating-point values, ele1 and ele2, by rounding them + to the specified precision and checking if the rounded values are equal. It returns + True if the rounded values are equal within the specified precision, and False otherwise. + """ + + return round(float(ele1), precision) == round(float(ele2), precision) + + def is_area_updated(self, updated_site, requested_site): + """ + Check if the area site details have been updated. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - updated_site (dict): The site details after the update. + - requested_site (dict): The site details as requested for the update. + Return: + bool: True if the area details (name and parentName) have been updated, False otherwise. + Description: + This method compares the area details (name and parentName) of the updated site + with the requested site and returns True if they are equal, indicating that the area + details have been updated. Returns False if there is a mismatch in the area site details. + """ + + return ( + updated_site['name'] == requested_site['name'] and + updated_site['parentName'] == requested_site['parentName'] + ) + + def is_building_updated(self, updated_site, requested_site): + """ + Check if the building details in a site have been updated. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - updated_site (dict): The site details after the update. + - requested_site (dict): The site details as requested for the update. + Return: + bool: True if the building details have been updated, False otherwise. + Description: + This method compares the building details of the updated site with the requested site. + It checks if the name, parentName, latitude, longitude, and address (if provided) are + equal, indicating that the building details have been updated. Returns True if the + details match, and False otherwise. + """ + + return ( + updated_site['name'] == requested_site['name'] and + updated_site['parentName'] == requested_site['parentName'] and + self.compare_float_values(updated_site['latitude'], requested_site['latitude']) and + self.compare_float_values(updated_site['longitude'], requested_site['longitude']) and + (requested_site['address'] is None or updated_site['address'] == requested_site['address']) + ) + + def is_floor_updated(self, updated_site, requested_site): + """ + Check if the floor details in a site have been updated. + + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - updated_site (dict): The site details after the update. + - requested_site (dict): The site details as requested for the update. + Return: + bool: True if the floor details have been updated, False otherwise. + Description: + This method compares the floor details of the updated site with the requested site. + It checks if the name, rf_model, length, width, and height are equal, indicating + that the floor details have been updated. Returns True if the details match, and False otherwise. + """ + + keys_to_compare = ['length', 'width', 'height'] + if updated_site['name'] != requested_site['name'] or updated_site['rf_model'] != requested_site['rfModel']: + return False + + for key in keys_to_compare: + if not self.compare_float_values(updated_site[key], requested_site[key]): + return False + + return True + + def site_requires_update(self): + """ + Check if the site requires updates. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + bool: True if the site requires updates, False otherwise. + Description: + This method compares the site parameters of the current site + ('current_site') and the requested site parameters ('requested_site') + stored in the 'want' attribute. It checks for differences in + specified parameters, such as the site type and site details. + """ + + type = self.have['current_site']['type'] + updated_site = self.have['current_site']['site'][type] + requested_site = self.want['site_params']['site'][type] + self.log("Current Site type: {0}".format(str(updated_site)), "INFO") + self.log("Requested Site type: {0}".format(str(requested_site)), "INFO") + + if type == "building": + return not self.is_building_updated(updated_site, requested_site) + + elif type == "floor": + return not self.is_floor_updated(updated_site, requested_site) + + return not self.is_area_updated(updated_site, requested_site) + + def get_have(self, config): + """ + Get the site details from Cisco Catalyst Center + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): A dictionary containing the configuration details. + Returns: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method queries Cisco Catalyst Center to check if a specified site + exists. If the site exists, it retrieves details about the current + site, including the site ID and other relevant information. The + results are stored in the 'have' attribute for later reference. + """ + + site_exists = False + current_site = None + have = {} + + # check if given site exits, if exists store current site info + (site_exists, current_site) = self.site_exists() + + self.log("Current Site details (have): {0}".format(str(current_site)), "DEBUG") + + if site_exists: + have["site_id"] = current_site.get("siteId") + have["site_exists"] = site_exists + have["current_site"] = current_site + + self.have = have + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + + return self + + def get_want(self, config): + """ + Get all site-related information from the playbook needed for creation/updation/deletion of site in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): A dictionary containing configuration information. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + Retrieves all site-related information from playbook that is + required for creating a site in Cisco Catalyst Center. It includes + parameters such as 'site_params' and 'site_name.' The gathered + information is stored in the 'want' attribute for later reference. + """ + + want = {} + want = dict( + site_params=self.get_site_params(config), + site_name=self.get_site_name(config), + ) + self.want = want + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + return self + + def get_diff_merged(self, config): + """ + Update/Create site information in Cisco Catalyst Center with fields + provided in the playbook. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): A dictionary containing configuration information. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method determines whether to update or create a site in Cisco Catalyst Center based on the provided + configuration information. If the specified site exists, the method checks if it requires an update + by calling the 'site_requires_update' method. If an update is required, it calls the 'update_site' + function from the 'sites' family of the Cisco Catalyst Center API. If the site does not require an update, + the method exits, indicating that the site is up to date. + """ + + site_updated = False + site_created = False + + # check if the given site exists and/or needs to be updated/created. + if self.have.get("site_exists"): + if self.site_requires_update(): + # Existing Site requires update + site_params = self.want.get("site_params") + site_params["site_id"] = self.have.get("site_id") + + response = self.dnac._exec( + family="sites", + function='update_site', + op_modifies=True, + params=site_params, + ) + site_updated = True + + else: + # Site does not neet update + self.result['response'] = self.have.get("current_site") + self.msg = "Site - {0} does not need any update".format(self.have.get("current_site")) + self.log(self.msg, "INFO") + self.result['msg'] = self.msg + return self + + else: + # Creating New Site + site_params = self.want.get("site_params") + response = self.dnac._exec( + family="sites", + function='create_site', + op_modifies=True, + params=self.want.get("site_params"), + ) + self.log("Received API response from 'create_site': {0}".format(str(response)), "DEBUG") + site_created = True + + if site_created or site_updated: + if response and isinstance(response, dict): + executionid = response.get("executionId") + while True: + execution_details = self.get_execution_details(executionid) + if execution_details.get("status") == "SUCCESS": + self.result['changed'] = True + self.result['response'] = execution_details + break + + elif execution_details.get("bapiError"): + self.module.fail_json(msg=execution_details.get("bapiError"), + response=execution_details) + break + + if site_updated: + log_msg = "Site - {0} Updated Successfully".format(self.want.get("site_name")) + self.log(log_msg, "INFO") + self.result['msg'] = log_msg + self.result['response'].update({"siteId": self.have.get("site_id")}) + + else: + # Get the site id of the newly created site. + (site_exists, current_site) = self.site_exists() + + if site_exists: + log_msg = "Site '{0}' created successfully".format(self.want.get("site_name")) + self.log(log_msg, "INFO") + self.log("Current site (have): {0}".format(str(current_site)), "DEBUG") + self.result['msg'] = log_msg + self.result['response'].update({"siteId": current_site.get('site_id')}) + + return self + + def delete_single_site(self, site_id, site_name): + """" + Delete a single site in the Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + site_id (str): The ID of the site to be deleted. + site_name (str): The name of the site to be deleted. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function initiates the deletion of a site in the Cisco Catalyst Center by calling the delete API. + If the deletion is successful, the result is marked as changed, and the status is set to "success." + If an error occurs during the deletion process, the status is set to "failed," and the log contains + details about the error. + """ + + try: + response = self.dnac._exec( + family="sites", + function="delete_site", + params={"site_id": site_id}, + ) + + if response and isinstance(response, dict): + self.log("Received API response from 'delete_site': {0}".format(str(response)), "DEBUG") + executionid = response.get("executionId") + + while True: + execution_details = self.get_execution_details(executionid) + if execution_details.get("status") == "SUCCESS": + self.msg = "Site '{0}' deleted successfully".format(site_name) + self.result['changed'] = True + self.result['response'] = self.msg + self.status = "success" + self.log(self.msg, "INFO") + break + elif execution_details.get("bapiError"): + self.log("Error response for 'delete_site' execution: {0}".format(execution_details.get("bapiError")), "ERROR") + self.module.fail_json(msg=execution_details.get("bapiError"), response=execution_details) + break + + except Exception as e: + self.status = "failed" + self.msg = "Exception occurred while deleting site '{0}' due to: {1}".format(site_name, str(e)) + self.log(self.msg, "ERROR") + + return self + + def get_diff_deleted(self, config): + """ + Call Cisco Catalyst Center API to delete sites with provided inputs. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): Dictionary containing information for site deletion. + Returns: + - self: The result dictionary includes the following keys: + - 'changed' (bool): Indicates whether changes were made + during the deletion process. + - 'response' (dict): Contains details about the execution + and the deleted site ID. + - 'msg' (str): A message indicating the status of the deletion operation. + Description: + This method initiates the deletion of a site by calling the 'delete_site' function in the 'sites' family + of the Cisco Catalyst Center API. It uses the site ID obtained from the 'have' attribute. + """ + + site_exists = self.have.get("site_exists") + site_name = self.want.get("site_name") + if not site_exists: + self.status = "success" + self.msg = "Unable to delete site '{0}' as it's not found in Cisco Catalyst Center".format(site_name) + self.result.update({'changed': False, + 'response': self.msg, + 'msg': self.msg}) + self.log(self.msg, "INFO") + + return self + + # Check here if the site have the childs then fetch it using get membership API and then sort it + # in reverse order and start deleting from bottom to top + site_id = self.have.get("site_id") + mem_response = self.dnac._exec( + family="sites", + function="get_membership", + params={"site_id": site_id}, + ) + site_response = mem_response.get("site").get("response") + self.log("Site {0} response along with it's child sites: {1}".format(site_name, str(site_response)), "DEBUG") + + if len(site_response) == 0: + self.delete_single_site(site_id, site_name) + return self + + # Sorting the response in reverse order based on hierarchy levels + sorted_site_resp = sorted(site_response, key=lambda x: x.get("groupHierarchy"), reverse=True) + + # Deleting each level in reverse order till topmost parent site + for item in sorted_site_resp: + self.delete_single_site(item['id'], item['name']) + + # Delete the final parent site + self.delete_single_site(site_id, site_name) + self.msg = "The site '{0}' and its child sites have been deleted successfully".format(site_name) + self.result['response'] = self.msg + self.log(self.msg, "INFO") + + return self + + def verify_diff_merged(self, config): + """ + Verify the merged status(Creation/Updation) of site configuration in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the merged status of a configuration in Cisco Catalyst Center by retrieving the current state + (have) and desired state (want) of the configuration, logs the states, and validates whether the specified + site exists in the Catalyst Center configuration. + """ + + self.get_have(config) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + # Code to validate ccc config for merged state + site_exist = self.have.get("site_exists") + site_name = self.want.get("site_name") + + if site_exist: + self.status = "success" + self.msg = "The requested site '{0}' is present in the Cisco Catalyst Center and its creation has been verified.".format(site_name) + self.log(self.msg, "INFO") + + require_update = self.site_requires_update() + + if not require_update: + self.log("The update for site '{0}' has been successfully verified.".format(site_name), "INFO") + self. status = "success" + return self + + self.log("""The playbook input for site '{0}' does not align with the Cisco Catalyst Center, indicating that the merge task + may not have executed successfully.""".format(site_name), "INFO") + + return self + + def verify_diff_deleted(self, config): + """ + Verify the deletion status of site configuration in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified site exists in the Catalyst Center configuration. + """ + + self.get_have(config) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + # Code to validate ccc config for delete state + site_exist = self.have.get("site_exists") + + if not site_exist: + self.status = "success" + msg = """The requested site '{0}' has already been deleted from the Cisco Catalyst Center and this has been + successfully verified.""".format(self.want.get("site_name")) + self.log(msg, "INFO") + return self + self.log("""Mismatch between the playbook input for site '{0}' and the Cisco Catalyst Center indicates that + the deletion was not executed successfully.""".format(self.want.get("site_name")), "INFO") + + return self + + +def main(): + """ main entry point for module execution + """ + + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {'type': 'bool', "default": False}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } + + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + + ccc_site = Site(module) + state = ccc_site.params.get("state") + + if state not in ccc_site.supported_states: + ccc_site.status = "invalid" + ccc_site.msg = "State {0} is invalid".format(state) + ccc_site.check_return_status() + + ccc_site.validate_input().check_return_status() + config_verify = ccc_site.params.get("config_verify") + + for config in ccc_site.validated_config: + ccc_site.reset_values() + ccc_site.get_want(config).check_return_status() + ccc_site.get_have(config).check_return_status() + ccc_site.get_diff_state_apply[state](config).check_return_status() + if config_verify: + ccc_site.verify_diff_state_apply[state](config).check_return_status() + + module.exit_json(**ccc_site.result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py new file mode 100644 index 0000000000..8e1f49187d --- /dev/null +++ b/plugins/modules/swim_workflow_manager.py @@ -0,0 +1,1717 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ("Madhan Sankaranarayanan, Rishita Chowdhary, Abhishek Maheshwari") + +DOCUMENTATION = r""" +--- +module: swim_workflow_manager +short_description: Intent module for SWIM related functions +description: +- Manage operation related to image importation, distribution, activation and tagging image as golden +- API to fetch a software image from remote file system using URL for HTTP/FTP and upload it to Catalyst Center. + Supported image files extensions are bin, img, tar, smu, pie, aes, iso, ova, tar_gz and qcow2. +- API to fetch a software image from local file system and upload it to Catalyst Center + Supported image files extensions are bin, img, tar, smu, pie, aes, iso, ova, tar_gz and qcow2. +- API to tag/untag image as golen for a given family of devices +- API to distribute a software image on a given device. Software image must be imported successfully into + Catalyst Center before it can be distributed. +- API to activate a software image on a given device. Software image must be present in the device flash. +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Madhan Sankaranarayanan (@madhansansel) + Rishita Chowdhary (@rishitachowdhary) + Abhishek Maheshwari (@abmahesh) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. + type: bool + default: False + state: + description: The state of Catalyst Center after module completion. + type: str + choices: [ merged ] + default: merged + config: + description: List of details of SWIM image being managed + type: list + elements: dict + required: True + suboptions: + import_image_details: + description: Details of image being imported + type: dict + suboptions: + type: + description: The source of import, supports url import or local import. + type: str + local_image_details: + description: Details of the local path of the image to be imported. + type: dict + suboptions: + file_path: + description: File absolute path. + type: str + is_third_party: + description: IsThirdParty query parameter. Third party Image check. + type: bool + third_party_application_type: + description: ThirdPartyApplicationType query parameter. Third Party Application Type. + type: str + third_party_image_family: + description: ThirdPartyImageFamily query parameter. Third Party image family. + type: str + third_party_vendor: + description: ThirdPartyVendor query parameter. Third Party Vendor. + type: str + url_details: + description: URL details for SWIM import + type: dict + suboptions: + payload: + description: Swim Import Via Url's payload. + type: list + elements: dict + suboptions: + application_type: + description: Swim Import Via Url's applicationType. + type: str + image_family: + description: Swim Import Via Url's imageFamily. + type: str + source_url: + description: Swim Import Image Via Url. + type: str + is_third_party: + description: ThirdParty flag. + type: bool + vendor: + description: Swim Import Via Url's vendor. + type: str + schedule_at: + description: ScheduleAt query parameter. Epoch Time (The number of milli-seconds since + January 1 1970 UTC) at which the distribution should be scheduled (Optional). + type: str + schedule_desc: + description: ScheduleDesc query parameter. Custom Description (Optional). + type: str + schedule_origin: + description: ScheduleOrigin query parameter. Originator of this call (Optional). + type: str + tagging_details: + description: Details for tagging or untagging an image as golden + type: dict + suboptions: + image_name: + description: SWIM image name which will be tagged or untagged as golden. + type: str + device_role: + description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION and CORE. + type: str + device_family_name: + description: Device family name(Eg Switches and Hubs) + type: str + device_type: + description: Type of the device (Eg Cisco Catalyst 9300 Switch) + type: str + site_name: + description: Site name for which SWIM image will be tagged/untagged as golden. + If not provided, SWIM image will be mapped to global site. + type: str + tagging: + description: Booelan value to tag/untag SWIM image as golden + If True then the given image will be tagged as golden. + If False then the given image will be un-tagged as golden. + type: bool + image_distribution_details: + description: Details for SWIM image distribution. Device on which the image needs to distributed + can be speciifed using any of the following parameters - deviceSerialNumber, + deviceIPAddress, deviceHostname or deviceMacAddress. + type: dict + suboptions: + device_role: + description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION and CORE. + type: str + device_family_name: + description: Device family name + type: str + site_name: + description: Used to get device details associated to this site. + type: str + image_name: + description: SWIM image's name + type: str + device_serial_number: + description: Device serial number where the image needs to be distributed + type: str + device_ip_address: + description: Device IP address where the image needs to be distributed + type: str + device_hostname: + description: Device hostname where the image needs to be distributed + type: str + device_mac_address: + description: Device MAC address where the image needs to be distributed + type: str + image_activation_details: + description: Details for SWIM image activation. Device on which the image needs to activated + can be speciifed using any of the following parameters - deviceSerialNumber, + deviceIPAddress, deviceHostname or deviceMacAddress. + type: dict + suboptions: + device_role: + description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION and CORE. + type: str + device_family_name: + description: Device family name + type: str + site_name: + description: Used to get device details associated to this site. + type: str + activate_lower_image_version: + description: ActivateLowerImageVersion flag. + type: bool + device_upgrade_mode: + description: Swim Trigger Activation's deviceUpgradeMode. + type: str + distributeIfNeeded: + description: DistributeIfNeeded flag. + type: bool + image_name: + description: SWIM image's name + type: str + device_serial_number: + description: Device serial number where the image needs to be activated + type: str + device_ip_address: + description: Device IP address where the image needs to be activated + type: str + device_hostname: + description: Device hostname where the image needs to be activated + type: str + device_mac_address: + description: Device MAC address where the image needs to be activated + type: str + schedule_validate: + description: ScheduleValidate query parameter. ScheduleValidate, validates data + before schedule (Optional). + type: bool +requirements: +- dnacentersdk == 2.4.5 +- python >= 3.5 +notes: + - SDK Method used are + software_image_management_swim.SoftwareImageManagementSwim.import_software_image_via_url, + software_image_management_swim.SoftwareImageManagementSwim.tag_as_golden_image, + software_image_management_swim.SoftwareImageManagementSwim.trigger_software_image_distribution, + software_image_management_swim.SoftwareImageManagementSwim.trigger_software_image_activation, + + - Paths used are + post /dna/intent/api/v1/image/importation/source/url, + post /dna/intent/api/v1/image/importation/golden, + post /dna/intent/api/v1/image/distribution, + post /dna/intent/api/v1/image/activation/device, + +""" + +EXAMPLES = r""" +- name: Import an image from a URL, tag it as golden and load it on device + cisco.dnac.swim_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: True + config: + - import_image_details: + type: string + url_details: + payload: + - source_url: string + is_third_party: bool + image_family: string + vendor: string + application_type: string + schedule_at: string + schedule_desc: string + schedule_origin: string + tagging_details: + image_name: string + device_role: string + device_family_name: string + site_name: string + tagging: bool + image_distribution_details: + image_name: string + device_serial_number: string + image_activation_details: + schedule_validate: bool + activate_lower_image_version: bool + distribute_if_needed: bool + device_serial_number: string + image_name: string + +- name: Import an image from local, tag it as golden. + cisco.dnac.swim_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: True + config: + - import_image_details: + type: string + local_image_details: + file_path: string + is_third_party: bool + third_party_vendor: string + third_party_image_family: string + third_party_application_type: string + tagging_details: + image_name: string + device_role: string + device_family_name: string + device_type: string + site_name: string + tagging: bool + +- name: Tag the given image as golden and load it on device + cisco.dnac.swim_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: True + config: + - tagging_details: + image_name: string + device_role: string + device_type: string + site_name: string + tagging: true + +- name: Un-tagged the given image as golden and load it on device + cisco.dnac.swim_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: True + config: + - tagging_details: + image_name: string + device_role: string + device_type: string + site_name: string + tagging: false + +- name: Distribute the given image on devices associated to that site with specified role. + cisco.dnac.swim_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: True + config: + - image_distribution_details: + image_name: string + site_name: string + device_role: string + device_family_name: string + +- name: Activate the given image on devices associated to that site with specified role. + cisco.dnac.swim_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: True + config: + - image_activation_details: + image_name: string + site_name: string + device_role: string + device_family_name: string + scehdule_validate: bool + activate_lower_image_version: bool + distribute_if_needed: bool + +""" + +RETURN = r""" +#Case: SWIM image is successfully imported, tagged as golden, distributed and activated on a device +response: + description: A dictionary with activation details as returned by the Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": { + "additionalStatusURL": String, + "data": String, + "endTime": 0, + "id": String, + "instanceTenantId": String, + "isError": bool, + "lastUpdate": 0, + "progress": String, + "rootId": String, + "serviceType": String, + "startTime": 0, + "version": 0 + }, + "msg": String + } + +""" + +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, + get_dict_result, +) +from ansible.module_utils.basic import AnsibleModule +import os + + +class Swim(DnacBase): + """Class containing member attributes for Swim intent module""" + + def __init__(self, module): + super().__init__(module) + self.supported_states = ["merged"] + + def validate_input(self): + """ + Validate the fields provided in the playbook. + Checks the configuration provided in the playbook against a predefined specification + to ensure it adheres to the expected structure and data types. + Parameters: + - self: The instance of the class containing the 'config' attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of 'config' parameter. + Example: + To use this method, create an instance of the class and call 'validate_input' on it. + If the validation succeeds, 'self.status' will be 'success' and 'self.validated_config' + will contain the validated configuration. If it fails, 'self.status' will be 'failed', + 'self.msg' will describe the validation issues. + """ + + if not self.config: + self.status = "success" + self.msg = "Configuration is not available in the playbook for validation" + self.log(self.msg, "ERROR") + return self + + temp_spec = dict( + import_image_details=dict(type='dict'), + tagging_details=dict(type='dict'), + image_distribution_details=dict(type='dict'), + image_activation_details=dict(type='dict'), + ) + + # Validate swim params + valid_temp, invalid_params = validate_list_of_dicts( + self.config, temp_spec + ) + + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.log(self.msg, "ERROR") + self.status = "failed" + return self + + self.validated_config = valid_temp + self.msg = "Successfully validated playbook config params: {0}".format(str(valid_temp)) + self.log(self.msg, "INFO") + self.status = "success" + + return self + + def site_exists(self, site_name): + """ + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + tuple: A tuple containing two values: + - site_exists (bool): A boolean indicating whether the site exists (True) or not (False). + - site_id (str or None): The ID of the site if it exists, or None if the site is not found. + Description: + This method checks the existence of a site in the Catalyst Center. If the site is found,it sets 'site_exists' to True, + retrieves the site's ID, and returns both values in a tuple. If the site does not exist, 'site_exists' is set + to False, and 'site_id' is None. If an exception occurs during the site lookup, an exception is raised. + """ + + site_exists = False + site_id = None + response = None + try: + response = self.dnac._exec( + family="sites", + function='get_site', + params={"name": site_name}, + ) + except Exception as e: + self.log("An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center".format(site_name), "ERROR") + self.module.fail_json(msg="Site not found") + + if response: + self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") + site = response.get("response") + site_id = site[0].get("id") + site_exists = True + + return (site_exists, site_id) + + def get_image_id(self, name): + """ + Retrieve the unique image ID based on the provided image name. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + name (str): The name of the software image to search for. + Returns: + str: The unique image ID (UUID) corresponding to the given image name. + Raises: + AnsibleFailJson: If the image is not found in the response. + Description: + This function sends a request to Cisco Catalyst Center to retrieve details about a software image based on its name. + It extracts and returns the image ID if a single matching image is found. If no image or multiple + images are found with the same name, it raises an exception. + """ + + image_response = self.dnac._exec( + family="software_image_management_swim", + function='get_software_image_details', + params={"image_name": name}, + ) + self.log("Received API response from 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") + image_list = image_response.get("response") + + if (len(image_list) == 1): + image_id = image_list[0].get("imageUuid") + self.log("SWIM image '{0}' has the ID: {1}".format(name, image_id), "INFO") + else: + error_message = "SWIM image '{0}' could not be found".format(name) + self.log(error_message, "ERROR") + self.module.fail_json(msg=error_message, response=image_response) + + return image_id + + def get_image_name_from_id(self, image_id): + """ + Retrieve the unique image name based on the provided image id. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + id (str): The unique image ID (UUID) of the software image to search for. + Returns: + str: The image name corresponding to the given unique image ID (UUID) + Raises: + AnsibleFailJson: If the image is not found in the response. + Description: + This function sends a request to Cisco Catalyst Center to retrieve details about a software image based on its id. + It extracts and returns the image name if a single matching image is found. If no image or multiple + images are found with the same name, it raises an exception. + """ + + image_response = self.dnac._exec( + family="software_image_management_swim", + function='get_software_image_details', + params={"image_uuid": image_id}, + ) + self.log("Received API response from 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") + image_list = image_response.get("response") + + if (len(image_list) == 1): + image_name = image_list[0].get("name") + self.log("SWIM image '{0}' has been fetched successfully from Cisco Catalyst Center".format(image_name), "INFO") + else: + error_message = "SWIM image with Id '{0}' could not be found in Cisco Catalyst Center".format(image_id) + self.log(error_message, "ERROR") + self.module.fail_json(msg=error_message, response=image_response) + + return image_name + + def is_image_exist(self, name): + """ + Retrieve the unique image ID based on the provided image name. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + name (str): The name of the software image to search for. + Returns: + str: The unique image ID (UUID) corresponding to the given image name. + Raises: + AnsibleFailJson: If the image is not found in the response. + Description: + This function sends a request to Cisco Catalyst Center to retrieve details about a software image based on its name. + It extracts and returns the image ID if a single matching image is found. If no image or multiple + images are found with the same name, it raises an exception. + """ + + image_exist = False + image_response = self.dnac._exec( + family="software_image_management_swim", + function='get_software_image_details', + params={"image_name": name}, + ) + self.log("Received API response from 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") + image_list = image_response.get("response") + + if (len(image_list) == 1): + image_exist = True + + return image_exist + + def get_device_id(self, params): + """ + Retrieve the unique device ID based on the provided parameters. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + params (dict): A dictionary containing parameters to filter devices. + Returns: + str: The unique device ID corresponding to the filtered device. + Description: + This function sends a request to Cisco Catalyst Center to retrieve a list of devices based on the provided + filtering parameters. If a single matching device is found, it extracts and returns the device ID. If + no device or multiple devices match the criteria, it raises an exception. + """ + device_id = None + response = self.dnac._exec( + family="devices", + function='get_device_list', + params=params, + ) + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + + device_list = response.get("response") + if (len(device_list) == 1): + device_id = device_list[0].get("id") + self.log("Device Id: {0}".format(str(device_id)), "INFO") + else: + self.log("Device not found", "WARNING") + + return device_id + + def get_device_uuids(self, site_name, device_family, device_role): + """ + Retrieve a list of device UUIDs based on the specified criteria. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + site_name (str): The name of the site for which device UUIDs are requested. + device_family (str): The family/type of devices to filter on. + device_role (str): The role of devices to filter on. If None, 'ALL' roles are considered. + Returns: + list: A list of device UUIDs that match the specified criteria. + Description: + The function checks the reachability status and role of devices in the given site. + Only devices with "Reachable" status are considered, and filtering is based on the specified + device family and role (if provided). + """ + + device_uuid_list = [] + if not site_name: + return device_uuid_list + + (site_exists, site_id) = self.site_exists(site_name) + if not site_exists: + return device_uuid_list + + site_params = { + "site_id": site_id, + "device_family": device_family + } + response = self.dnac._exec( + family="sites", + function='get_membership', + op_modifies=True, + params=site_params, + ) + self.log("Received API response from 'get_membership': {0}".format(str(response)), "DEBUG") + response = response['device'][0]['response'] + + if len(response) > 0: + for item in response: + if item["reachabilityStatus"] != "Reachable": + continue + if "role" in item and (device_role is None or item["role"] == device_role.upper() or device_role.upper() == "ALL"): + device_uuid_list.append(item["instanceUuid"]) + + return device_uuid_list + + def get_device_family_identifier(self, family_name): + """ + Retrieve and store the device family identifier based on the provided family name. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + family_name (str): The name of the device family for which to retrieve the identifier. + Returns: + None + Raises: + AnsibleFailJson: If the family name is not found in the response. + Description: + This function sends a request to Cisco Catalyst Center to retrieve a list of device family identifiers.It then + searches for a specific family name within the response and stores its associated identifier. If the family + name is found, the identifier is stored; otherwise, an exception is raised. + """ + + have = {} + response = self.dnac._exec( + family="software_image_management_swim", + function='get_device_family_identifiers', + ) + self.log("Received API response from 'get_device_family_identifiers': {0}".format(str(response)), "DEBUG") + device_family_db = response.get("response") + + if device_family_db: + device_family_details = get_dict_result(device_family_db, 'deviceFamily', family_name) + + if device_family_details: + device_family_identifier = device_family_details.get("deviceFamilyIdentifier") + have["device_family_identifier"] = device_family_identifier + self.log("Family device indentifier: {0}".format(str(device_family_identifier)), "INFO") + else: + self.log("Device Family: {0} not found".format(str(family_name)), "ERROR") + self.module.fail_json(msg="Family Device Name not found", response=[]) + self.have.update(have) + + def get_have(self): + """ + Retrieve and store various software image and device details based on user-provided information. + Returns: + self: The current instance of the class with updated 'have' attributes. + Raises: + AnsibleFailJson: If required image or device details are not provided. + Description: + This function populates the 'have' dictionary with details related to software images, site information, + device families, distribution devices, and activation devices based on user-provided data in the 'want' dictionary. + It validates and retrieves the necessary information from Cisco Catalyst Center to support later actions. + """ + + if self.want.get("tagging_details"): + have = {} + tagging_details = self.want.get("tagging_details") + if tagging_details.get("image_name"): + name = tagging_details.get("image_name").split("/")[-1] + image_id = self.get_image_id(name) + have["tagging_image_id"] = image_id + + elif self.have.get("imported_image_id"): + have["tagging_image_id"] = self.have.get("imported_image_id") + + else: + self.log("Image details for tagging not provided", "CRITICAL") + self.module.fail_json(msg="Image details for tagging not provided", response=[]) + + # check if given site exists, store siteid + # if not then use global site + site_name = tagging_details.get("site_name") + if site_name: + site_exists = False + (site_exists, site_id) = self.site_exists(site_name) + if site_exists: + have["site_id"] = site_id + self.log("Site {0} exists having the site id: {1}".format(site_name, str(site_id)), "DEBUG") + else: + # For global site, use -1 as siteId + have["site_id"] = "-1" + self.log("Site Name not given by user. Using global site.", "WARNING") + + self.have.update(have) + # check if given device family name exists, store indentifier value + family_name = tagging_details.get("device_type") + self.get_device_family_identifier(family_name) + + if self.want.get("distribution_details"): + have = {} + distribution_details = self.want.get("distribution_details") + site_name = distribution_details.get("site_name") + if site_name: + site_exists = False + (site_exists, site_id) = self.site_exists(site_name) + + if site_exists: + have["site_id"] = site_id + self.log("Site '{0}' exists and has the site ID: {1}".format(site_name, str(site_id)), "DEBUG") + + # check if image for distributon is available + if distribution_details.get("image_name"): + name = distribution_details.get("image_name").split("/")[-1] + image_id = self.get_image_id(name) + have["distribution_image_id"] = image_id + + elif self.have.get("imported_image_id"): + have["distribution_image_id"] = self.have.get("imported_image_id") + + else: + self.log("Image details required for distribution have not been provided", "ERROR") + self.module.fail_json(msg="Image details required for distribution have not been provided", response=[]) + + device_params = dict( + hostname=distribution_details.get("device_hostname"), + serialNumber=distribution_details.get("device_serial_number"), + managementIpAddress=distribution_details.get("device_ip_address"), + macAddress=distribution_details.get("device_mac_address"), + ) + device_id = self.get_device_id(device_params) + if device_id is not None: + have["distribution_device_id"] = device_id + self.have.update(have) + + if self.want.get("activation_details"): + have = {} + activation_details = self.want.get("activation_details") + # check if image for activation is available + if activation_details.get("image_name"): + name = activation_details.get("image_name").split("/")[-1] + image_id = self.get_image_id(name) + have["activation_image_id"] = image_id + + elif self.have.get("imported_image_id"): + have["activation_image_id"] = self.have.get("imported_image_id") + else: + self.log("Image details required for activation have not been provided", "ERROR") + self.module.fail_json(msg="Image details required for activation have not been provided", response=[]) + + site_name = activation_details.get("site_name") + if site_name: + site_exists = False + (site_exists, site_id) = self.site_exists(site_name) + if site_exists: + have["site_id"] = site_id + self.log("The site '{0}' exists and has the site ID '{1}'".format(site_name, str(site_id)), "INFO") + + device_params = dict( + hostname=activation_details.get("device_hostname"), + serialNumber=activation_details.get("device_serial_number"), + managementIpAddress=activation_details.get("device_ip_address"), + macAddress=activation_details.get("device_mac_address"), + ) + device_id = self.get_device_id(device_params) + + if device_id is not None: + have["activation_device_id"] = device_id + self.have.update(have) + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + + return self + + def get_want(self, config): + """ + Retrieve and store import, tagging, distribution, and activation details from playbook configuration. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): The configuration dictionary containing image import and other details. + Returns: + self: The current instance of the class with updated 'want' attributes. + Raises: + AnsibleFailJson: If an incorrect import type is specified. + Description: + This function parses the playbook configuration to extract information related to image + import, tagging, distribution, and activation. It stores these details in the 'want' dictionary + for later use in the Ansible module. + """ + + want = {} + if config.get("import_image_details"): + want["import_image"] = True + want["import_type"] = config.get("import_image_details").get("type").lower() + if want["import_type"] == "url": + want["url_import_details"] = config.get("import_image_details").get("url_details") + elif want["import_type"] == "local": + want["local_import_details"] = config.get("import_image_details").get("local_image_details") + else: + self.log("The import type '{0}' provided is incorrect. Only 'local' or 'url' are supported.".format(want["import_type"]), "CRITICAL") + self.module.fail_json(msg="Incorrect import type. Supported Values: local or url") + + want["tagging_details"] = config.get("tagging_details") + want["distribution_details"] = config.get("image_distribution_details") + want["activation_details"] = config.get("image_activation_details") + + self.want = want + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + return self + + def get_diff_import(self): + """ + Check the image import type and fetch the image ID for the imported image for further use. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function checks the type of image import (URL or local) and proceeds with the import operation accordingly. + It then monitors the import task's progress and updates the 'result' dictionary. If the operation is successful, + 'changed' is set to True. + Additionally, if tagging, distribution, or activation details are provided, it fetches the image ID for the + imported image and stores it in the 'have' dictionary for later use. + """ + + try: + import_type = self.want.get("import_type") + + if not import_type: + self.status = "success" + self.msg = "Error: Details required for importing SWIM image. Please provide the necessary information." + self.result['msg'] = self.msg + self.log(self.msg, "WARNING") + self.result['changed'] = False + return self + + if import_type == "url": + image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") + else: + image_name = self.want.get("local_import_details").get("file_path") + + # Code to check if the image already exists in Catalyst Center + name = image_name.split('/')[-1] + image_exist = self.is_image_exist(name) + + import_key_mapping = { + 'source_url': 'sourceURL', + 'image_family': 'imageFamily', + 'application_type': 'applicationType', + 'is_third_party': 'thirdParty', + } + + if image_exist: + image_id = self.get_image_id(name) + self.have["imported_image_id"] = image_id + self.msg = "Image '{0}' already exists in the Cisco Catalyst Center".format(name) + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + self.status = "success" + self.result['changed'] = False + return self + + if self.want.get("import_type") == "url": + import_payload_dict = {} + temp_payload = self.want.get("url_import_details").get("payload")[0] + keys_to_change = list(import_key_mapping.keys()) + + for key, val in temp_payload.items(): + if key in keys_to_change: + api_key_name = import_key_mapping[key] + import_payload_dict[api_key_name] = val + + import_image_payload = [import_payload_dict] + import_params = dict( + payload=import_image_payload, + scheduleAt=self.want.get("url_import_details").get("schedule_at"), + scheduleDesc=self.want.get("url_import_details").get("schedule_desc"), + scheduleOrigin=self.want.get("url_import_details").get("schedule_origin"), + ) + import_function = 'import_software_image_via_url' + else: + file_path = self.want.get("local_import_details").get("file_path") + import_params = dict( + is_third_party=self.want.get("local_import_details").get("is_third_party"), + third_party_vendor=self.want.get("local_import_details").get("third_party_vendor"), + third_party_image_family=self.want.get("local_import_details").get("third_party_image_family"), + third_party_application_type=self.want.get("local_import_details").get("third_party_application_type"), + multipart_fields={'file': (os.path.basename(file_path), open(file_path, 'rb'), 'application/octet-stream')}, + multipart_monitor_callback=None + ) + import_function = 'import_local_software_image' + + response = self.dnac._exec( + family="software_image_management_swim", + function=import_function, + op_modifies=True, + params=import_params, + ) + self.log("Received API response from {0}: {1}".format(import_function, str(response)), "DEBUG") + + task_details = {} + task_id = response.get("response").get("taskId") + + while (True): + task_details = self.get_task_details(task_id) + name = image_name.split('/')[-1] + + if task_details and \ + ("completed successfully" in task_details.get("progress").lower()): + self.result['changed'] = True + self.status = "success" + self.msg = "Swim Image {0} imported successfully".format(name) + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + break + + if task_details and task_details.get("isError"): + if "already exists" in task_details.get("failureReason", ""): + self.msg = "SWIM Image {0} already exists in the Cisco Catalyst Center".format(name) + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + self.status = "success" + self.result['changed'] = False + break + else: + self.status = "failed" + self.msg = task_details.get("failureReason", "SWIM Image {0} seems to be invalid".format(image_name)) + self.log(self.msg, "WARNING") + self.result['response'] = self.msg + return self + + self.result['response'] = task_details if task_details else response + + # Fetch image_id for the imported image for further use + image_name = image_name.split('/')[-1] + image_id = self.get_image_id(image_name) + self.have["imported_image_id"] = image_id + + return self + + except Exception as e: + self.status = "failed" + self.msg = """Error: Import image details are not provided in the playbook, or the Import Image API was not + triggered successfully. Please ensure the necessary details are provided and verify the status of the Import Image process.""" + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + + return self + + def get_diff_tagging(self): + """ + Tag or untag a software image as golden based on provided tagging details. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function tags or untags a software image as a golden image in Cisco Catalyst Center based on the provided + tagging details. The tagging action is determined by the value of the 'tagging' attribute + in the 'tagging_details' dictionary.If 'tagging' is True, the image is tagged as golden, and if 'tagging' + is False, the golden tag is removed. The function sends the appropriate request to Cisco Catalyst Center and updates the + task details in the 'result' dictionary. If the operation is successful, 'changed' is set to True. + """ + + tagging_details = self.want.get("tagging_details") + tag_image_golden = tagging_details.get("tagging") + image_name = self.get_image_name_from_id(self.have.get("tagging_image_id")) + + image_params = dict( + image_id=self.have.get("tagging_image_id"), + site_id=self.have.get("site_id"), + device_family_identifier=self.have.get("device_family_identifier"), + device_role=tagging_details.get("device_role", "ALL").upper() + ) + + response = self.dnac._exec( + family="software_image_management_swim", + function='get_golden_tag_status_of_an_image', + op_modifies=True, + params=image_params + ) + self.log("Received API response from 'get_golden_tag_status_of_an_image': {0}".format(str(response)), "DEBUG") + + response = response.get('response') + if response: + image_status = response['taggedGolden'] + if image_status and image_status == tag_image_golden: + self.status = "success" + self.result['changed'] = False + self.msg = "SWIM Image '{0}' already tagged as Golden image in Cisco Catalyst Center".format(image_name) + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + return self + + if not image_status and image_status == tag_image_golden: + self.status = "success" + self.result['changed'] = False + self.msg = "SWIM Image '{0}' already un-tagged from Golden image in Cisco Catalyst Center".format(image_name) + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + return self + + if tag_image_golden: + image_params = dict( + imageId=self.have.get("tagging_image_id"), + siteId=self.have.get("site_id"), + deviceFamilyIdentifier=self.have.get("device_family_identifier"), + deviceRole=tagging_details.get("device_role", "ALL").upper() + ) + self.log("Parameters for tagging the image as golden: {0}".format(str(image_params)), "INFO") + + response = self.dnac._exec( + family="software_image_management_swim", + function='tag_as_golden_image', + op_modifies=True, + params=image_params + ) + self.log("Received API response from 'tag_as_golden_image': {0}".format(str(response)), "DEBUG") + + else: + self.log("Parameters for un-tagging the image as golden: {0}".format(str(image_params)), "INFO") + + response = self.dnac._exec( + family="software_image_management_swim", + function='remove_golden_tag_for_image', + op_modifies=True, + params=image_params + ) + self.log("Received API response from 'remove_golden_tag_for_image': {0}".format(str(response)), "DEBUG") + + if response: + task_details = {} + task_id = response.get("response").get("taskId") + task_details = self.get_task_details(task_id) + if not task_details.get("isError"): + self.result['changed'] = True + self.result['msg'] = task_details.get("progress") + self.status = "success" + self.result['response'] = task_details if task_details else response + elif task_details.get("isError"): + failure_reason = task_details.get("failureReason", "") + if failure_reason and "An inheritted tag cannot be un-tagged" in failure_reason: + self.status = "success" + self.result['changed'] = False + self.msg = failure_reason + self.result['msg'] = failure_reason + self.log(self.msg, "WARNING") + else: + error_message = task_details.get("failureReason", "Error: while tagging/un-tagging the golden swim image.") + self.status = "failed" + self.msg = error_message + self.result['msg'] = error_message + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + + return self + + def get_device_ip_from_id(self, device_id): + """ + Retrieve the management IP address of a device from Cisco Catalyst Center using its ID. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - device_id (str): The unique identifier of the device in Cisco Catalyst Center. + Returns: + str: The management IP address of the specified device. + Raises: + Exception: If there is an error while retrieving the response from Cisco Catalyst Center. + Description: + This method queries Cisco Catalyst Center for the device details based on its unique identifier (ID). + It uses the 'get_device_list' function in the 'devices' family, extracts the management IP address + from the response, and returns it. If any error occurs during the process, an exception is raised + with an appropriate error message logged. + """ + + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"id": device_id} + ) + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + response = response.get('response')[0] + device_ip = response.get("managementIpAddress") + + return device_ip + except Exception as e: + error_message = "Error occurred while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + + def get_diff_distribution(self): + """ + Get image distribution parameters from the playbook and trigger image distribution. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function retrieves image distribution parameters from the playbook's 'distribution_details' and triggers + the distribution of the specified software image to the specified device. It monitors the distribution task's + progress and updates the 'result' dictionary. If the operation is successful, 'changed' is set to True. + """ + + distribution_details = self.want.get("distribution_details") + site_name = distribution_details.get("site_name") + device_family = distribution_details.get("device_family_name") + device_role = distribution_details.get("device_role", "ALL") + device_uuid_list = self.get_device_uuids(site_name, device_family, device_role) + image_id = self.have.get("distribution_image_id") + + if self.have.get("distribution_device_id"): + self.single_device_distribution = False + distribution_params = dict( + payload=[dict( + deviceUuid=self.have.get("distribution_device_id"), + imageUuid=image_id + )] + ) + self.log("Distribution Params: {0}".format(str(distribution_params)), "INFO") + + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_distribution', + op_modifies=True, + params=distribution_params, + ) + self.log("Received API response from 'trigger_software_image_distribution': {0}".format(str(response)), "DEBUG") + + if response: + task_details = {} + task_id = response.get("response").get("taskId") + + while (True): + task_details = self.get_task_details(task_id) + + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.status = "success" + self.single_device_distribution = True + self.result['msg'] = "Image with Id {0} Distributed Successfully".format(image_id) + break + + if task_details.get("isError"): + self.status = "failed" + self.msg = "Image with Id {0} Distribution Failed".format(image_id) + self.log(self.msg, "WARNING") + self.result['response'] = task_details + break + + self.result['response'] = task_details if task_details else response + + return self + + if len(device_uuid_list) == 0: + self.status = "failed" + self.msg = "Image Distribution cannot proceed due to the absence of device(s)" + self.result['msg'] = self.msg + self.log(self.msg, "WARNING") + return self + + self.log("Device UUIDs involved in Image Distribution: {0}".format(str(device_uuid_list)), "INFO") + + device_distribution_count = 0 + device_ips_list = [] + self.complete_successful_distribution = False + self.partial_successful_distribution = False + + for device_uuid in device_uuid_list: + device_management_ip = self.get_device_ip_from_id(device_uuid) + distribution_params = dict( + payload=[dict( + deviceUuid=device_uuid, + imageUuid=image_id + )] + ) + self.log("Distribution Params: {0}".format(str(distribution_params)), "INFO") + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_distribution', + op_modifies=True, + params=distribution_params, + ) + self.log("Received API response from 'trigger_software_image_distribution': {0}".format(str(response)), "DEBUG") + + if response: + task_details = {} + task_id = response.get("response").get("taskId") + + while (True): + task_details = self.get_task_details(task_id) + + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id '{0}' Distributed successfully".format(image_id) + device_distribution_count += 1 + break + + if task_details.get("isError"): + error_msg = "Image with Id '{0}' Distribution failed".format(image_id) + self.log(error_msg, "WARNING") + self.result['response'] = task_details + device_ips_list.append(device_management_ip) + break + + if device_distribution_count == 0: + self.status = "failed" + self.msg = "Image with Id {0} Distribution Failed for all devices".format(image_id) + elif device_distribution_count == len(device_uuid_list): + self.result['changed'] = True + self.status = "success" + self.complete_successful_distribution = True + self.msg = "Image with Id {0} Distributed Successfully for all devices".format(image_id) + else: + self.result['changed'] = True + self.status = "success" + self.partial_successful_distribution = False + self.msg = "Image with Id '{0}' Distributed and partially successfull".format(image_id) + self.log("For device(s) {0} image Distribution gets failed".format(str(device_ips_list)), "CRITICAL") + + self.result['msg'] = self.msg + self.log(self.msg, "INFO") + + return self + + def get_diff_activation(self): + """ + Get image activation parameters from the playbook and trigger image activation. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function retrieves image activation parameters from the playbook's 'activation_details' and triggers the + activation of the specified software image on the specified device. It monitors the activation task's progress and + updates the 'result' dictionary. If the operation is successful, 'changed' is set to True. + """ + + activation_details = self.want.get("activation_details") + site_name = activation_details.get("site_name") + device_family = activation_details.get("device_family_name") + device_role = activation_details.get("device_role", "ALL") + device_uuid_list = self.get_device_uuids(site_name, device_family, device_role) + image_id = self.have.get("activation_image_id") + + if self.have.get("activation_device_id"): + self.single_device_activation = False + payload = [dict( + activateLowerImageVersion=activation_details.get("activate_lower_image_version"), + deviceUpgradeMode=activation_details.get("device_upgrade_mode"), + distributeIfNeeded=activation_details.get("distribute_if_needed"), + deviceUuid=self.have.get("activation_device_id"), + imageUuidList=[image_id] + )] + + activation_params = dict( + schedule_validate=activation_details.get("scehdule_validate"), + payload=payload + ) + self.log("Activation Params: {0}".format(str(activation_params)), "INFO") + + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_activation', + op_modifies=True, + params=activation_params, + ) + self.log("Received API response from 'trigger_software_image_activation': {0}".format(str(response)), "DEBUG") + + task_details = {} + task_id = response.get("response").get("taskId") + + while (True): + task_details = self.get_task_details(task_id) + + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.result['msg'] = "Image Activated successfully" + self.status = "success" + self.single_device_activation = True + break + + if task_details.get("isError"): + error_msg = "Activation for Image with Id '{0}' gets failed".format(image_id) + self.status = "failed" + self.result['response'] = task_details + self.msg = error_msg + self.log(error_msg, "WARNING") + return self + + self.result['response'] = task_details if task_details else response + + return self + + if len(device_uuid_list) == 0: + self.status = "failed" + self.msg = "No devices found for Image Activation" + self.result['msg'] = self.msg + self.log(self.msg, "WARNING") + return self + + self.log("Device UUIDs involved in Image Activation: {0}".format(str(device_uuid_list)), "INFO") + device_activation_count = 0 + device_ips_list = [] + self.complete_successful_activation = False + self.partial_successful_activation = False + + for device_uuid in device_uuid_list: + device_management_ip = self.get_device_ip_from_id(device_uuid) + payload = [dict( + activateLowerImageVersion=activation_details.get("activate_lower_image_version"), + deviceUpgradeMode=activation_details.get("device_upgrade_mode"), + distributeIfNeeded=activation_details.get("distribute_if_needed"), + deviceUuid=device_uuid, + imageUuidList=[image_id] + )] + + activation_params = dict( + schedule_validate=activation_details.get("scehdule_validate"), + payload=payload + ) + self.log("Activation Params: {0}".format(str(activation_params)), "INFO") + + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_activation', + op_modifies=True, + params=activation_params, + ) + self.log("Received API response from 'trigger_software_image_activation': {0}".format(str(response)), "DEBUG") + + if response: + task_details = {} + task_id = response.get("response").get("taskId") + + while (True): + task_details = self.get_task_details(task_id) + + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id '{0}' activated successfully".format(image_id) + device_activation_count += 1 + break + + if task_details.get("isError"): + error_msg = "Image with Id '{0}' activation failed".format(image_id) + self.log(error_msg, "WARNING") + self.result['response'] = task_details + device_ips_list.append(device_management_ip) + break + + if device_activation_count == 0: + self.status = "failed" + msg = "Image with Id '{0}' activation failed for all devices".format(image_id) + elif device_activation_count == len(device_uuid_list): + self.result['changed'] = True + self.status = "success" + self.complete_successful_activation = True + msg = "Image with Id '{0}' activated successfully for all devices".format(image_id) + else: + self.result['changed'] = True + self.status = "success" + self.partial_successful_activation = True + msg = "Image with Id '{0}' activated and partially successfull".format(image_id) + self.log("For Device(s) {0} Image activation gets Failed".format(str(device_ips_list)), "CRITICAL") + + self.result['msg'] = msg + self.log(msg, "INFO") + + return self + + def get_diff_merged(self, config): + """ + Get tagging details and then trigger distribution followed by activation if specified in the playbook. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): The configuration dictionary containing tagging, distribution, and activation details. + Returns: + self: The current instance of the class with updated 'result' and 'have' attributes. + Description: + This function checks the provided playbook configuration for tagging, distribution, and activation details. It + then triggers these operations in sequence if the corresponding details are found in the configuration.The + function monitors the progress of each task and updates the 'result' dictionary accordingly. If any of the + operations are successful, 'changed' is set to True. + """ + + if config.get("tagging_details"): + self.get_diff_tagging().check_return_status() + + if config.get("image_distribution_details"): + self.get_diff_distribution().check_return_status() + + if config.get("image_activation_details"): + self.get_diff_activation().check_return_status() + + return self + + def verify_diff_imported(self, import_type): + """ + Verify the successful import of a software image into Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + import_type (str): The type of import, either 'url' or 'local'. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the successful import of a software image into Cisco Catalyst Center. + It checks whether the image exists in Catalyst Center based on the provided import type. + If the image exists, the status is set to 'success', and a success message is logged. + If the image does not exist, a warning message is logged indicating a potential import failure. + """ + + if import_type == "url": + image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") + else: + image_name = self.want.get("local_import_details").get("file_path") + + # Code to check if the image already exists in Catalyst Center + name = image_name.split('/')[-1] + image_exist = self.is_image_exist(name) + if image_exist: + self.status = "success" + self.msg = "The requested Image '{0}' imported in the Cisco Catalyst Center and Image presence has been verified.".format(name) + self.log(self.msg, "INFO") + else: + self.log("""The playbook input for SWIM Image '{0}' does not align with the Cisco Catalyst Center, indicating that image + may not have imported successfully.""".format(name), "INFO") + + return self + + def verify_diff_tagged(self): + """ + Verify the Golden tagging status of a software image in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the tagging status of a software image in Cisco Catalyst Center. + It retrieves tagging details from the input, including the desired tagging status and image ID. + Using the provided image ID, it obtains image parameters required for checking the image status. + The method then queries Catalyst Center to get the golden tag status of the image. + If the image status matches the desired tagging status, a success message is logged. + If there is a mismatch between the playbook input and the Catalyst Center, a warning message is logged. + """ + + tagging_details = self.want.get("tagging_details") + tag_image_golden = tagging_details.get("tagging") + image_id = self.have.get("tagging_image_id") + image_name = self.get_image_name_from_id(image_id) + + image_params = dict( + image_id=self.have.get("tagging_image_id"), + site_id=self.have.get("site_id"), + device_family_identifier=self.have.get("device_family_identifier"), + device_role=tagging_details.get("device_role", "ALL").upper() + ) + self.log("Parameters for checking the status of image: {0}".format(str(image_params)), "INFO") + + response = self.dnac._exec( + family="software_image_management_swim", + function='get_golden_tag_status_of_an_image', + op_modifies=True, + params=image_params + ) + self.log("Received API response from 'get_golden_tag_status_of_an_image': {0}".format(str(response)), "DEBUG") + + response = response.get('response') + if response: + image_status = response['taggedGolden'] + if image_status == tag_image_golden: + if tag_image_golden: + self.msg = """The requested image '{0}' has been tagged as golden in the Cisco Catalyst Center and + its status has been successfully verified.""".format(image_name) + self.log(self.msg, "INFO") + else: + self.msg = """The requested image '{0}' has been un-tagged as golden in the Cisco Catalyst Center and + image status has been verified.""".format(image_name) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook input for tagging/un-tagging image as golden and the Cisco Catalyst Center indicates that + the tagging/un-tagging task was not executed successfully.""", "INFO") + + return self + + def verify_diff_distributed(self): + """ + Verify the distribution status of a software image in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + import_type (str): The type of import, either 'url' or 'local'. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the distribution status of a software image in Cisco Catalyst Center. + It retrieves the image ID and name from the input and if distribution device ID is provided, it checks the distribution status for that + list of specific device and logs the info message based on distribution status. + """ + + image_id = self.have.get("distribution_image_id") + image_name = self.get_image_name_from_id(image_id) + + if self.have.get("distribution_device_id"): + if self.single_device_distribution: + self.msg = """The requested image '{0}', associated with the device ID '{1}', has been successfully distributed in the Cisco Catalyst Center + and its status has been verified.""".format(image_name, self.have.get("distribution_device_id")) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook input for distributing the image to the device with ID '{0}' and the actual state in the + Cisco Catalyst Center suggests that the distribution task might not have been executed + successfully.""".format(self.have.get("distribution_device_id")), "INFO") + elif self.complete_successful_distribution: + self.msg = """The requested image '{0}', with ID '{1}', has been successfully distributed to all devices within the specified + site in the Cisco Catalyst Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + elif self.partial_successful_distribution: + self.msg = """T"The requested image '{0}', with ID '{1}', has been partially distributed across some devices in the Cisco Catalyst + Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + else: + self.msg = """The requested image '{0}', with ID '{1}', failed to be distributed across devices in the Cisco Catalyst + Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + + return self + + def verify_diff_activated(self): + """ + Verify the activation status of a software image in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the activation status of a software image in Cisco Catalyst Center and retrieves the image ID and name from + the input. If activation device ID is provided, it checks the activation status for that specific device. Based on activation status + a corresponding message is logged. + """ + + image_id = self.have.get("activation_image_id") + image_name = self.get_image_name_from_id(image_id) + + if self.have.get("activation_device_id"): + if self.single_device_activation: + self.msg = """The requested image '{0}', associated with the device ID '{1}', has been successfully activated in the Cisco Catalyst + Center and its status has been verified.""".format(image_name, self.have.get("activation_device_id")) + self.log(self.msg, "INFO") + else: + self.log("""Mismatch between the playbook's input for activating the image '{0}' on the device with ID '{1}' and the actual state in + the Cisco Catalyst Center suggests that the activation task might not have been executed + successfully.""".format(image_name, self.have.get("activation_device_id")), "INFO") + elif self.complete_successful_activation: + self.msg = """The requested image '{0}', with ID '{1}', has been successfully activated on all devices within the specified site in the + Cisco Catalyst Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + elif self.partial_successful_activation: + self.msg = """"The requested image '{0}', with ID '{1}', has been partially activated on some devices in the Cisco + Catalyst Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + else: + self.msg = """The activation of the requested image '{0}', with ID '{1}', failed on devices in the Cisco + Catalyst Center.""".format(image_name, image_id) + self.log(self.msg, "INFO") + + return self + + def verify_diff_merged(self, config): + """ + Verify the merged status(Importing/Tagging/Distributing/Actiavting) the SWIM Image in devices in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the merged status of a configuration in Cisco Catalyst Center by retrieving the current state + (have) and desired state (want) of the configuration, logs the states, and validates whether the specified + SWIM operation performed or not. + """ + + self.get_have() + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + import_type = self.want.get("import_type") + if import_type: + self.verify_diff_imported(import_type).check_return_status() + + tagged = self.want.get("tagging_details") + if tagged: + self.verify_diff_tagged().check_return_status() + + distribution_details = self.want.get("distribution_details") + if distribution_details: + self.verify_diff_distributed().check_return_status() + + activation_details = self.want.get("activation_details") + if activation_details: + self.verify_diff_activated().check_return_status() + + return self + + +def main(): + """ main entry point for module execution + """ + + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {'type': 'bool', "default": False}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged']} + } + + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + + ccc_swims = Swim(module) + state = ccc_swims.params.get("state") + + if state not in ccc_swims.supported_states: + ccc_swims.status = "invalid" + ccc_swims.msg = "State {0} is invalid".format(state) + ccc_swims.check_return_status() + + ccc_swims.validate_input().check_return_status() + config_verify = ccc_swims.params.get("config_verify") + + for config in ccc_swims.validated_config: + ccc_swims.reset_values() + ccc_swims.get_want(config).check_return_status() + ccc_swims.get_diff_import().check_return_status() + ccc_swims.get_have().check_return_status() + ccc_swims.get_diff_state_apply[state](config).check_return_status() + if config_verify: + ccc_swims.verify_diff_state_apply[state](config).check_return_status() + + module.exit_json(**ccc_swims.result) + + +if __name__ == '__main__': + main() From 134ee1b58096be2f3eac73b99fdee892647204a8 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 6 Feb 2024 15:57:41 +0000 Subject: [PATCH 08/64] Fixes for SNMP issue and records to return value --- plugins/modules/discovery_intent.py | 68 +++++++++++++++++------------ 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index e5fe2cd82e..5e8dcfbc82 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -71,34 +71,34 @@ description: HTTP read credentials for hosting a device type: dict suboptions: + username: + description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + type: str password: - description: HTTP(S) password and is mandatory for using HTTP credentials. + description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. type: str port: - description: HTTP(S) port and is mandatory for using HTTP credentials. + description: The HTTP(S) port number, which is mandatory for using HTTP credentials. type: int secure: - description: Flag for HTTP(S) and is not mandatory for using HTTP credentials. + description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. type: bool - username: - description: HTTP(S) username and is mandatory for using HTTP credentials. - type: str http_write_credential: description: HTTP write credentials for hosting a device type: dict suboptions: + username: + description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + type: str password: - description: HTTP(S) password and is mandatory for using HTTP credentials. + description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. type: str port: - description: HTTP(S) port and is mandatory for using HTTP credentials. + description: The HTTP(S) port number, which is mandatory for using HTTP credentials. type: int secure: - description: Flag for HTTP(S) and is not mandatory for using HTTP credentials. + description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. type: bool - username: - description: HTTP(S) username and is mandatory for using HTTP credentials. - type: str ip_filter_list: description: List of IP adddrsess that needs to get filtered out from the IP addresses added type: list @@ -574,6 +574,14 @@ def preprocess_device_discovery_handle_error(self): self.log("IP Address list's length is longer than 1", "ERROR") self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) + def http_cred_failure(self, msg=None): + """ + Method for failing discovery if there is any discrepancy in the http credentials + passed by the user + """ + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + def create_params(self, credential_ids=None, ip_address_list=None): """ Create a new parameter object based on the validated configuration, @@ -597,27 +605,31 @@ def create_params(self, credential_ids=None, ip_address_list=None): http_write_credential = self.validated_config[0].get('http_write_credential') if http_read_credential: if not (http_read_credential.get('password') and isinstance(http_read_credential.get('password'), str)): - msg = "Http Read Credential must have a password of type string" + msg = "The password for the HTTP read credential must be of string type." + self.http_cred_failure(msg=msg) if not (http_read_credential.get('username') and isinstance(http_read_credential.get('username'), str)): - msg = "Http Read Credential must have a username of type string" - if not (http_read_credential.get('password') and isinstance(http_read_credential.get('password'), int)): - msg = "Http Read Credential must have port of type integer" + msg = "The username for the HTTP read credential must be of string type." + self.http_cred_failure(msg=msg) + if not (http_read_credential.get('port') and isinstance(http_read_credential.get('port'), int)): + msg = "The port for the HTTP read Credential must be of integer type." + self.http_cred_failure(msg=msg) if not isinstance(http_read_credential.get('secure'), bool): - msg = "Secure must be of type bool" - self.log(msg, "CRITICAL") - self.module.fail_json(msg=msg) + msg = "Secure for HTTP read Credential must be of type boolean." + self.http_cred_failure(msg=msg) if http_write_credential: - if not (http_write_credential.get('password') and isinstance(http_read_credential.get('password'), str)): - msg = "Http Write Credential must have a password of type string" - if not (http_write_credential.get('username') and isinstance(http_read_credential.get('username'), str)): - msg = "Http Write Credential must have a username of type string" - if not (http_write_credential.get('password') and isinstance(http_read_credential.get('password'), int)): - msg = "Http Write Credential must have port of type integer" + if not (http_write_credential.get('password') and isinstance(http_write_credential.get('password'), str)): + msg = "The password for the HTTP write credential must be of string type." + self.http_cred_failure(msg=msg) + if not (http_write_credential.get('username') and isinstance(http_write_credential.get('username'), str)): + msg = "The username for the HTTP write credential must be of string type." + self.http_cred_failure(msg=msg) + if not (http_write_credential.get('port') and isinstance(http_write_credential.get('port'), int)): + msg = "The port for the HTTP write Credential must be of integer type." + self.http_cred_failure(msg=msg) if not isinstance(http_write_credential.get('secure'), bool): - msg = "Secure must be of type bool" - self.log(msg, "CRITICAL") - self.module.fail_json(msg=msg) + msg = "Secure for HTTP write Credential must be of type boolean." + self.http_cred_failure(msg=msg) new_object_params = {} new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') From 46b1c7515daebc48d59a44d33de734b2ea2ea909 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 7 Feb 2024 00:22:14 +0530 Subject: [PATCH 09/64] Removed the unnecessary comments from the playbooks --- playbooks/device_credential_workflow_manager.yml | 4 ---- playbooks/network_settings_workflow_manager.yml | 4 ---- playbooks/template_workflow_manager.yml | 3 --- 3 files changed, 11 deletions(-) diff --git a/playbooks/device_credential_workflow_manager.yml b/playbooks/device_credential_workflow_manager.yml index 08a7a97951..3d77584f06 100644 --- a/playbooks/device_credential_workflow_manager.yml +++ b/playbooks/device_credential_workflow_manager.yml @@ -4,10 +4,6 @@ gather_facts: no connection: local tasks: -# -# Project Info Section -# - - name: Create Credentials and assign it to a site. cisco.dnac.device_credential_workflow_manager: dnac_host: "{{ dnac_host }}" diff --git a/playbooks/network_settings_workflow_manager.yml b/playbooks/network_settings_workflow_manager.yml index 40fb93c29b..84d965cb14 100644 --- a/playbooks/network_settings_workflow_manager.yml +++ b/playbooks/network_settings_workflow_manager.yml @@ -4,10 +4,6 @@ gather_facts: no connection: local tasks: -# -# Project Info Section -# - - name: Create global pool, reserve subpool and network functions cisco.dnac.network_settings_workflow_manager: dnac_host: "{{ dnac_host }}" diff --git a/playbooks/template_workflow_manager.yml b/playbooks/template_workflow_manager.yml index 35a7a60d2d..25b0ec7975 100644 --- a/playbooks/template_workflow_manager.yml +++ b/playbooks/template_workflow_manager.yml @@ -5,9 +5,6 @@ gather_facts: false connection: local tasks: -# -# Project Info Section -# - name: Test project template cisco.dnac.template_workflow_manager: dnac_host: "{{ dnac_host }}" From 1592b3baee3fcb5332710ffc1f3884e6f9503965 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 7 Feb 2024 10:34:46 +0530 Subject: [PATCH 10/64] remove commented line from the site, swim playbook and update comments in Inventory and swim module --- playbooks/inventory_workflow_manager.yml | 5 +---- playbooks/site_workflow_manager.yml | 8 -------- playbooks/swim_workflow_manager.yml | 4 ---- plugins/modules/site_workflow_manager.py | 2 +- plugins/modules/swim_workflow_manager.py | 2 +- 5 files changed, 3 insertions(+), 18 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index 8f7028eca4..2ef5acbcfc 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -22,13 +22,10 @@ state: merged config: - username: "{{item.username}}" - enable_password: "{{item.enable_password}}" password: "{{item.password}}" + enable_password: "{{item.enable_password}}" ip_address: "{{item.ip_address}}" cli_transport: "{{item.cli_transport}}" - # hostname_list: "{{item.hostname_list}}" - # serial_number_list: "{{item.serial_number_list}}" - # mac_address_list: "{{item.mac_address_list}}" snmp_auth_passphrase: "{{item.snmp_auth_passphrase}}" snmp_auth_protocol: "{{item.snmp_auth_protocol}}" snmp_mode: "{{item.snmp_mode}}" diff --git a/playbooks/site_workflow_manager.yml b/playbooks/site_workflow_manager.yml index b35ad4a8d1..1c4b52ac8a 100644 --- a/playbooks/site_workflow_manager.yml +++ b/playbooks/site_workflow_manager.yml @@ -37,11 +37,3 @@ longitude: -42.1234434 country: "United States" type: area - - # For deleting a site - # - site: - # floor: - # name: Test_Floor2 - # parent_name: 'Global/USA/San Francisco/BGL_18' - # type: floor - diff --git a/playbooks/swim_workflow_manager.yml b/playbooks/swim_workflow_manager.yml index d09cdce00e..237fcaec4b 100644 --- a/playbooks/swim_workflow_manager.yml +++ b/playbooks/swim_workflow_manager.yml @@ -21,10 +21,6 @@ config_verify: true config: - import_image_details: - # type: "local" - # local_image_details: - # file_path: "/Users/abmahesh/Downloads/cat9k_iosxe.17.12.01.SPA.bin" - # is_third_party: false type: "{{ item.type }}" url_details: payload: diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index fedd8ad4e5..aaaae97a45 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -323,7 +323,7 @@ class Site(DnacBase): - """Class containing member attributes for site intent module""" + """Class containing member attributes for Site workflow_manager module""" def __init__(self, module): super().__init__(module) diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 8e1f49187d..d6a7127021 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -409,7 +409,7 @@ class Swim(DnacBase): - """Class containing member attributes for Swim intent module""" + """Class containing member attributes for Swim workflow_manager module""" def __init__(self, module): super().__init__(module) From 352e536165aa952e9aeadb8131cb7a90d6945a64 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 7 Feb 2024 12:10:46 +0530 Subject: [PATCH 11/64] Added a global documentation file for all the workflow_manager modules --- .../doc_fragments/workflow_manager_params.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 plugins/doc_fragments/workflow_manager_params.py diff --git a/plugins/doc_fragments/workflow_manager_params.py b/plugins/doc_fragments/workflow_manager_params.py new file mode 100644 index 0000000000..9c76aa25ac --- /dev/null +++ b/plugins/doc_fragments/workflow_manager_params.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +options: + dnac_host: + description: + - The Cisco DNA Center hostname. + type: str + required: true + dnac_port: + description: + - The Cisco DNA Center port. + type: str + default: '443' + dnac_username: + description: + - The Cisco DNA Center username to authenticate. + type: str + default: admin + aliases: [ user ] + dnac_password: + description: + - The Cisco DNA Center password to authenticate. + type: str + dnac_verify: + description: + - Flag to enable or disable SSL certificate verification. + type: bool + default: true + dnac_version: + description: + - Informs the SDK which version of Cisco DNA Center to use. + type: str + default: 2.2.3.3 + dnac_debug: + description: + - Flag for Cisco DNA Center SDK to enable debugging. + type: bool + default: false + dnac_log: + description: + - Flag to enable/disable playbook execution logging. + - When true and dnac_log_file_path is provided, + - Create the log file at the execution location with the specified name. + - When true and dnac_log_file_path is not provided, + - Create the log file at the execution location with the name 'dnac.log'. + - When false, + - Logging is disabled. + - If the log file doesn't exist, + - It is created in append or write mode based on the "dnac_log_append" flag. + - If the log file exists, + - It is overwritten or appended based on the "dnac_log_append" flag. + type: bool + default: false + dnac_log_level: + description: + - Sets the threshold for log level. Messages with a level equal to or higher than + this will be logged. Levels are listed in order of severity [CRITICAL, ERROR, WARNING, INFO, DEBUG]. + - CRITICAL indicates serious errors halting the program. Displays only CRITICAL messages. + - ERROR indicates problems preventing a function. Displays ERROR and CRITICAL messages. + - WARNING indicates potential future issues. Displays WARNING, ERROR, CRITICAL messages. + - INFO tracks normal operation. Displays INFO, WARNING, ERROR, CRITICAL messages. + - DEBUG provides detailed diagnostic info. Displays all log messages. + type: str + default: WARNING + dnac_log_file_path: + description: + - Governs logging. Logs are recorded if dnac_log is True. + - If path is not specified, + - When 'dnac_log_append' is True, 'dnac.log' is generated in the + current Ansible directory; logs are appended. + - When 'dnac_log_append' is False, 'dnac.log' is generated; logs + are overwritten. + - If path is specified, + - When 'dnac_log_append' is True, the file opens in append mode. + - When 'dnac_log_append' is False, the file opens in write (w) mode. + - In shared file scenarios, without append mode, content is + overwritten after each module execution. + - For a shared log file, set append to False for the 1st module + (to overwrite); for subsequent modules, set append to True. + type: str + default: dnac.log + dnac_log_append: + description: Determines the mode of the file. Set to True for 'append' mode. Set to False for 'write' mode. + type: bool + default: True + validate_response_schema: + description: + - Flag for Cisco DNA Center SDK to enable the validation of request bodies against a JSON schema. + type: bool + default: true +notes: + - "Does not support C(check_mode)" + - "The plugin runs on the control node and does not use any ansible connection plugins, but instead the embedded connection manager from Cisco DNAC SDK" + - "The parameters starting with dnac_ are used by the Cisco DNAC Python SDK to establish the connection" +''' From 52349282b5a5574702d795287eba67a228996809 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 7 Feb 2024 12:28:13 +0530 Subject: [PATCH 12/64] Changed the DNAC to Cisco Catalyst Center --- .../doc_fragments/workflow_manager_params.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/doc_fragments/workflow_manager_params.py b/plugins/doc_fragments/workflow_manager_params.py index 9c76aa25ac..15069f4768 100644 --- a/plugins/doc_fragments/workflow_manager_params.py +++ b/plugins/doc_fragments/workflow_manager_params.py @@ -6,6 +6,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +__author__ = ['Madhan Sankaranarayanan, Muthu Rakesh'] class ModuleDocFragment(object): @@ -15,23 +16,23 @@ class ModuleDocFragment(object): options: dnac_host: description: - - The Cisco DNA Center hostname. + - The Cisco Catalyst Center hostname. type: str required: true dnac_port: description: - - The Cisco DNA Center port. + - The Cisco Catalyst Center port. type: str default: '443' dnac_username: description: - - The Cisco DNA Center username to authenticate. + - The Cisco Catalyst Center username to authenticate. type: str default: admin aliases: [ user ] dnac_password: description: - - The Cisco DNA Center password to authenticate. + - The Cisco Catalyst Center password to authenticate. type: str dnac_verify: description: @@ -40,12 +41,12 @@ class ModuleDocFragment(object): default: true dnac_version: description: - - Informs the SDK which version of Cisco DNA Center to use. + - Informs the SDK which version of Cisco Catalyst Center to use. type: str default: 2.2.3.3 dnac_debug: description: - - Flag for Cisco DNA Center SDK to enable debugging. + - Flag for Cisco Catalyst Center SDK to enable debugging. type: bool default: false dnac_log: @@ -97,11 +98,11 @@ class ModuleDocFragment(object): default: True validate_response_schema: description: - - Flag for Cisco DNA Center SDK to enable the validation of request bodies against a JSON schema. + - Flag for Cisco Catalyst Center SDK to enable the validation of request bodies against a JSON schema. type: bool default: true notes: - "Does not support C(check_mode)" - - "The plugin runs on the control node and does not use any ansible connection plugins, but instead the embedded connection manager from Cisco DNAC SDK" - - "The parameters starting with dnac_ are used by the Cisco DNAC Python SDK to establish the connection" + - "The plugin runs on the control node and does not use any ansible connection plugins instead embedded connection manager from Cisco Catalyst Center SDK" + - "The parameters starting with dnac_ are used by the Cisco Catalyst Center Python SDK to establish the connection" ''' From d3df7d1dfb029c79c0e9f336a25b847a02f00203 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 7 Feb 2024 12:38:57 +0530 Subject: [PATCH 13/64] Changed DNAC to Cisco Catalyst Center --- plugins/module_utils/dnac.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 4b00a72dcd..7229de0421 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -81,7 +81,7 @@ def __init__(self, module): # If dnac_log is False, return an empty logger self.logger = logging.getLogger('empty_logger') - self.log('Dnac parameters: {0}'.format(dnac_params), "DEBUG") + self.log('Cisco Catalyst Center parameters: {0}'.format(dnac_params), "DEBUG") self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -237,7 +237,7 @@ def is_valid_password(self, password): """ Check if a password is valid. Args: - self (object): An instance of a class that provides access to Cisco DNA Center. + self (object): An instance of a class that provides access to Cisco Catalyst Center. password (str): The password to be validated. Returns: bool: True if the password is valid, False otherwise. @@ -255,7 +255,7 @@ def is_valid_password(self, password): return re.match(pattern, password) is not None def get_dnac_params(self, params): - """Store the DNAC parameters from the playbook""" + """Store the Cisco Catalyst Center parameters from the playbook""" dnac_params = {"dnac_host": params.get("dnac_host"), "dnac_port": params.get("dnac_port"), @@ -272,15 +272,15 @@ def get_dnac_params(self, params): def get_task_details(self, task_id): """ - Get the details of a specific task in Cisco DNA Center. + Get the details of a specific task in Cisco Catalyst Center. Args: - self (object): An instance of a class that provides access to Cisco DNA Center. + self (object): An instance of a class that provides access to Cisco Catalyst Center. task_id (str): The unique identifier of the task for which you want to retrieve details. Returns: dict or None: A dictionary containing detailed information about the specified task, or None if the task with the given task_id is not found. Description: - If the task with the specified task ID is not found in Cisco DNA Center, this function will return None. + If the task with the specified task ID is not found in Cisco Catalyst Center, this function will return None. """ result = None @@ -379,7 +379,7 @@ def get_execution_details(self, execid): def check_execution_response_status(self, response): """ - Checks the reponse status provided by API in the DNAC + Checks the reponse status provided by API in the Cisco Catalyst Center Parameters: response (dict) - API response From 500035407d5fc15022fc6868fa5483d52789f6f3 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 7 Feb 2024 14:27:20 +0530 Subject: [PATCH 14/64] Addressed the PR comments --- plugins/doc_fragments/workflow_manager_params.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/doc_fragments/workflow_manager_params.py b/plugins/doc_fragments/workflow_manager_params.py index 15069f4768..7aa7253797 100644 --- a/plugins/doc_fragments/workflow_manager_params.py +++ b/plugins/doc_fragments/workflow_manager_params.py @@ -16,23 +16,23 @@ class ModuleDocFragment(object): options: dnac_host: description: - - The Cisco Catalyst Center hostname. + - The hostname of the Cisco Catalyst Center. type: str required: true dnac_port: description: - - The Cisco Catalyst Center port. + - Specifies the port number associated with the Cisco Catalyst Center. type: str default: '443' dnac_username: description: - - The Cisco Catalyst Center username to authenticate. + - The username for authentication at the Cisco Catalyst Center. type: str default: admin aliases: [ user ] dnac_password: description: - - The Cisco Catalyst Center password to authenticate. + - The password for authentication at the Cisco Catalyst Center. type: str dnac_verify: description: @@ -41,12 +41,12 @@ class ModuleDocFragment(object): default: true dnac_version: description: - - Informs the SDK which version of Cisco Catalyst Center to use. + - Specifies the version of the Cisco Catalyst Center that the SDK should use. type: str default: 2.2.3.3 dnac_debug: description: - - Flag for Cisco Catalyst Center SDK to enable debugging. + - Indicates whether debugging is enabled in the Cisco Catalyst Center SDK. type: bool default: false dnac_log: From bd3d71f825b402043f952cc9aa2271633ecf8a16 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 7 Feb 2024 15:29:33 +0530 Subject: [PATCH 15/64] Changed the global documentation from intent_params to workflow_manager_params --- plugins/modules/device_credential_workflow_manager.py | 2 +- plugins/modules/network_settings_workflow_manager.py | 2 +- plugins/modules/template_workflow_manager.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py index cd5b44cae3..a2ab0ec229 100644 --- a/plugins/modules/device_credential_workflow_manager.py +++ b/plugins/modules/device_credential_workflow_manager.py @@ -21,7 +21,7 @@ - API to assign the device credential to the site. version_added: '6.7.0' extends_documentation_fragment: - - cisco.dnac.intent_params + - cisco.dnac.workflow_manager_params author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 1c0539ab52..5ab7f56e3e 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -21,7 +21,7 @@ and/or DNS center server settings. version_added: '6.6.0' extends_documentation_fragment: - - cisco.dnac.intent_params + - cisco.dnac.workflow_manager_params author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index 7df5c0fb7f..702d1781ad 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -24,7 +24,7 @@ - API to manage operation create of the resource Configuration Template Import Template. version_added: '6.6.0' extends_documentation_fragment: - - cisco.dnac.intent_params + - cisco.dnac.workflow_manager_params author: Madhan Sankaranarayanan (@madhansansel) Rishita Chowdhary (@rishitachowdhary) Akash Bhaskaran (@akabhask) From 0bcdd8fb8ed676d6889c86626ceb540f57331c9e Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 7 Feb 2024 16:42:29 +0530 Subject: [PATCH 16/64] Changed the term intent to workflow_manager in the class descripion and changed the global documentation to workflow_manager_params from intent_params --- plugins/modules/device_credential_workflow_manager.py | 2 +- plugins/modules/inventory_workflow_manager.py | 2 +- plugins/modules/network_settings_workflow_manager.py | 2 +- plugins/modules/site_workflow_manager.py | 2 +- plugins/modules/swim_workflow_manager.py | 4 ++-- plugins/modules/template_workflow_manager.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/modules/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py index a2ab0ec229..f0c87bff1b 100644 --- a/plugins/modules/device_credential_workflow_manager.py +++ b/plugins/modules/device_credential_workflow_manager.py @@ -711,7 +711,7 @@ class DeviceCredential(DnacBase): - """Class containing member attributes for device credential intent module""" + """Class containing member attributes for device_credential_workflow_manager module""" def __init__(self, module): super().__init__(module) diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index f5ac9ead76..1fa51c23e8 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -20,7 +20,7 @@ - Sync the devices provided as input. version_added: '6.8.0' extends_documentation_fragment: - - cisco.dnac.intent_params + - cisco.dnac.workflow_manager_params author: Abhishek Maheshwari (@abmahesh) Madhan Sankaranarayanan (@madhansansel) options: diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 5ab7f56e3e..feb82c8ffd 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -416,7 +416,7 @@ class NetworkSettings(DnacBase): - """Class containing member attributes for network intent module""" + """Class containing member attributes for network_settings_workflow_manager module""" def __init__(self, module): super().__init__(module) diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index aaaae97a45..979a9d76db 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -20,7 +20,7 @@ - Deletes site with area/building/floor with specified hierarchy. version_added: '6.6.0' extends_documentation_fragment: - - cisco.dnac.intent_params + - cisco.dnac.workflow_manager_params author: Madhan Sankaranarayanan (@madhansansel) Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abhishekmaheshwari) diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index d6a7127021..ae49b5000a 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -12,7 +12,7 @@ DOCUMENTATION = r""" --- module: swim_workflow_manager -short_description: Intent module for SWIM related functions +short_description: workflow_manager module for SWIM related functions description: - Manage operation related to image importation, distribution, activation and tagging image as golden - API to fetch a software image from remote file system using URL for HTTP/FTP and upload it to Catalyst Center. @@ -25,7 +25,7 @@ - API to activate a software image on a given device. Software image must be present in the device flash. version_added: '6.6.0' extends_documentation_fragment: - - cisco.dnac.intent_params + - cisco.dnac.workflow_manager_params author: Madhan Sankaranarayanan (@madhansansel) Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abmahesh) diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index 702d1781ad..67f665190f 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -1306,7 +1306,7 @@ class Template(DnacBase): - """Class containing member attributes for template intent module""" + """Class containing member attributes for template_workflow_manager module""" def __init__(self, module): super().__init__(module) From e58229861e75f2adceeea2950f13cf632189d6b9 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 7 Feb 2024 16:56:02 +0530 Subject: [PATCH 17/64] Changed the copyright year to 2024 --- plugins/modules/device_credential_workflow_manager.py | 2 +- plugins/modules/inventory_workflow_manager.py | 2 +- plugins/modules/network_settings_workflow_manager.py | 2 +- plugins/modules/site_workflow_manager.py | 2 +- plugins/modules/swim_workflow_manager.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py index f0c87bff1b..7ae841d6f2 100644 --- a/plugins/modules/device_credential_workflow_manager.py +++ b/plugins/modules/device_credential_workflow_manager.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright (c) 2023, Cisco Systems +# Copyright (c) 2024, Cisco Systems # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) """Ansible module to perform operations on device credentials in Cisco Catalyst Center.""" diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 1fa51c23e8..2330395929 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright (c) 2022, Cisco Systems +# Copyright (c) 2024, Cisco Systems # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index feb82c8ffd..5ef8354832 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright (c) 2023, Cisco Systems +# Copyright (c) 2024, Cisco Systems # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) """Ansible module to perform operations on global pool, reserve pool and network in Cisco Catalyst Center.""" diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index 979a9d76db..3c0974c222 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright (c) 2022, Cisco Systems +# Copyright (c) 2024, Cisco Systems # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index ae49b5000a..daa826daec 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright (c) 2022, Cisco Systems +# Copyright (c) 2024, Cisco Systems # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function From 10d4e8e0e8f29716b890b463cd0fc803c36ee40a Mon Sep 17 00:00:00 2001 From: Abinash Date: Wed, 7 Feb 2024 14:17:51 +0000 Subject: [PATCH 18/64] Adding workflow manager codes for Discovery and PnP --- plugins/modules/discovery_workflow_manager.py | 1137 ++++++++++++++++ plugins/modules/pnp_workflow_manager.py | 1142 +++++++++++++++++ 2 files changed, 2279 insertions(+) create mode 100644 plugins/modules/discovery_workflow_manager.py create mode 100644 plugins/modules/pnp_workflow_manager.py diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py new file mode 100644 index 0000000000..b480deac86 --- /dev/null +++ b/plugins/modules/discovery_workflow_manager.py @@ -0,0 +1,1137 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ("Abinash Mishra, Phan Nguyen, Madhan Sankaranarayanan") + +DOCUMENTATION = r""" +--- +module: discovery_workflow_manager +short_description: Resource module for discovery related functions +description: +- Manage operations discover devices using IP address/range, CDP, LLDP and delete discoveries +- API to discover a device or multiple devices +- API to delete a discovery of a device or multiple devices +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.workflow_manager_params +author: Abinash Mishra (@abimishr) + Phan Nguyen (@phannguy) + Madhan Sankaranarayanan (@madsanka) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. + type: bool + default: False + state: + description: The state of Cisco Catalyst Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of device being managed. + type: list + elements: dict + required: true + suboptions: + ip_address_list: + description: List of IP addresses to be discovered. For CDP/LLDP/SINGLE based discovery, we should + pass a list with single element like - 10.197.156.22. For CIDR based discovery, we should pass a list with + single element like - 10.197.156.22/22. For RANGE based discovery, we should pass a list with single element + and range like - 10.197.156.1-10.197.156.100. For MULTI RANGE based discovery, we should pass a list with multiple + elementd like - 10.197.156.1-10.197.156.100 and in next line - 10.197.157.1-10.197.157.100. + type: list + elements: str + required: true + discovery_type: + description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP) + type: str + required: true + cdp_level: + description: Total number of levels that are there in cdp's method of discovery + type: int + default: 16 + lldp_level: + description: Total number of levels that are there in lldp's method of discovery + type: int + default: 16 + start_index: + description: Start index for the header in fetching SNMP v2 credentials + type: int + default: 1 + enable_password_list: + description: List of enable passwords for the CLI crfedentials + type: list + elements: str + records_to_return: + description: Number of records to return for the header in fetching global v2 credentials + type: int + default: 100 + http_read_credential: + description: HTTP read credentials for hosting a device + type: dict + suboptions: + username: + description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + type: str + password: + description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + type: str + port: + description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + type: int + secure: + description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + type: bool + http_write_credential: + description: HTTP write credentials for hosting a device + type: dict + suboptions: + username: + description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + type: str + password: + description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + type: str + port: + description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + type: int + secure: + description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + type: bool + ip_filter_list: + description: List of IP adddrsess that needs to get filtered out from the IP addresses added + type: list + elements: str + discovery_name: + description: Name of the discovery task + type: str + required: true + netconf_port: + description: Port for the netconf credentials + type: str + password_list: + description: List of passwords for the CLI credentials + type: list + elements: str + username_list: + description: List of passwords for the CLI credentials + type: list + elements: str + preferred_mgmt_ip_method: + description: Preferred method for the management of the IP (None/UseLoopBack) + type: str + default: None + protocol_order: + description: Order of protocol (ssh/telnet) in which device connection will be tried. For example, 'telnet' - only telnet - 'ssh, + telnet' - ssh with higher order than telnet + type: str + retry: + description: Number of times to try establishing connection to device + type: int + snmp_auth_passphrase: + description: Auth Pass phrase for SNMP + type: str + snmp_auth_protocol: + description: SNMP auth protocol (SHA/MD5) + type: str + snmp_mode: + description: Mode of SNMP (AUTHPRIV/AUTHNOPRIV/NOAUTHNOPRIV) + type: str + snmp_priv_passphrase: + description: Pass phrase for SNMP privacy + type: str + snmp_priv_protocol: + description: SNMP privacy protocol (DES/AES128) + type: str + snmp_ro_community: + description: Snmp RO community of the devices to be discovered + type: str + snmp_ro_community_desc: + description: Description for Snmp RO community + type: str + snmp_rw_community: + description: Snmp RW community of the devices to be discovered + type: str + snmp_rw_community_desc: + description: Description for Snmp RW community + type: str + snmp_username: + description: SNMP username of the device + type: str + snmp_version: + description: Version of SNMP (v2/v3) + type: str + timeout: + description: Time to wait for device response in seconds + type: int + cli_cred_len: + description: Specifies the total number of CLI credentials to be used, ranging from 1 to 5. + type: int + default: 1 + delete_all: + description: Parameter to delete all the discoveries at one go + type: bool + default: False +requirements: +- dnacentersdk == 2.6.10 +- python >= 3.5 +notes: + - SDK Method used are + discovery.Discovery.get_all_global_credentials_v2, + discovery.Discovery.start_discovery, + task.Task.get_task_by_id, + discovery.Discovery.get_discoveries_by_range, + discovery.Discovery.get_discovered_network_devices_by_discovery_id', + discovery.Discovery.delete_discovery_by_id + discovery.Discovery.delete_all_discovery + discovery.Discovery.get_count_of_all_discovery_jobs + + - Paths used are + get /dna/intent/api/v2/global-credential + post /dna/intent/api/v1/discovery + get /dna/intent/api/v1/task/{taskId} + get /dna/intent/api/v1/discovery/{startIndex}/{recordsToReturn} + get /dna/intent/api/v1/discovery/{id}/network-device + delete /dna/intent/api/v1/discovery/{id} + delete /dna/intent/api/v1/delete + get /dna/intent/api/v1/discovery/count + +""" + +EXAMPLES = r""" +- name: Execute discovery devices + cisco.dnac.discovery_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: merged + config_verify: True + config: + - ip_address_list: list + discovery_type: string + cdp_level: string + lldp_level: string + start_index: integer + enable_password_list: list + records_to_return: integer + http_read_credential: dictionary + http_write_credential: dictionary + ip_filter_list: list + discovery_name: string + password_list: list + preffered_mgmt_ip_method: string + protocol_order: string + retry: integer + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_ro_community: string + snmp_ro_community_desc: string + snmp_rw_community: string + snmp_rw_community_desc: string + snmp_username: string + snmp_version: string + timeout: integer + username_list: list + cli_cred_len: integer +- name: Delete disovery by name + cisco.dnac.discovery_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: deleted + config_verify: True + config: + - discovery_name: string +""" + +RETURN = r""" +#Case_1: When the device(s) are discovered successfully. +response_1: + description: A dictionary with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": + { + "response": String, + "version": String + }, + "msg": String + } + +#Case_2: Given device details or SNMP mode are not provided +response_2: + description: A list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: list + sample: > + { + "response": [], + "msg": String + } + +#Case_3: Error while deleting a discovery +response_3: + description: A string with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": String, + "msg": String + } +""" +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts +) +import time +import re + + +class Discovery(DnacBase): + def __init__(self, module): + """ + Initialize an instance of the class. It also initializes an empty + list for 'creds_ids_list' attribute. + + Parameters: + - module: The module associated with the class instance. + + Returns: + The method does not return a value. Instead, it initializes the + following instance attributes: + - self.creds_ids_list: An empty list that will be used to store + credentials IDs. + """ + + super().__init__(module) + self.creds_ids_list = [] + + def validate_input(self, state=None): + """ + Validate the fields provided in the playbook. Checks the + configuration provided in the playbook against a predefined + specification to ensure it adheres to the expected structure + and data types. + + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of the + 'config' parameter. + Example: + To use this method, create an instance of the class and call + 'validate_input' on it.If the validation succeeds, 'self.status' + will be 'success'and 'self.validated_config' will contain the + validated configuration. If it fails, 'self.status' will be + 'failed', and 'self.msg' will describe the validation issues. + """ + + if not self.config: + self.msg = "config not available in playbook for validation" + self.status = "success" + return self + + discovery_spec = { + 'cdp_level': {'type': 'int', 'required': False, + 'default': 16}, + 'enable_password_list': {'type': 'list', 'required': False, + 'elements': 'str'}, + 'start_index': {'type': 'int', 'required': False, + 'default': 1}, + 'records_to_return': {'type': 'int', 'required': False, + 'default': 100}, + 'http_read_credential': {'type': 'dict', 'required': False}, + 'http_write_credential': {'type': 'dict', 'required': False}, + 'ip_filter_list': {'type': 'list', 'required': False, + 'elements': 'str'}, + 'lldp_level': {'type': 'int', 'required': False, + 'default': 16}, + 'discovery_name': {'type': 'str', 'required': True}, + 'netconf_port': {'type': 'str', 'required': False}, + 'password_list': {'type': 'list', 'required': False, + 'elements': 'str'}, + 'preferred_mgmt_ip_method': {'type': 'str', 'required': False, + 'default': 'None'}, + 'protocol_order': {'type': 'str', 'required': False}, + 'retry': {'type': 'int', 'required': False}, + 'snmp_auth_passphrase': {'type': 'str', 'required': False}, + 'snmp_auth_protocol': {'type': 'str', 'required': False}, + 'snmp_mode': {'type': 'str', 'required': False}, + 'snmp_priv_passphrase': {'type': 'str', 'required': False}, + 'snmp_priv_protocol': {'type': 'str', 'required': False}, + 'snmp_ro_community': {'type': 'str', 'required': False}, + 'snmp_ro_community_desc': {'type': 'str', 'required': False}, + 'snmp_rw_community': {'type': 'str', 'required': False}, + 'snmp_rw_community_desc': {'type': 'str', 'required': False}, + 'snmp_username': {'type': 'str', 'required': False}, + 'snmp_version': {'type': 'str', 'required': False}, + 'timeout': {'type': 'str', 'required': False}, + 'username_list': {'type': 'list', 'required': False, + 'elements': 'str'}, + 'cli_cred_len': {'type': 'int', 'required': False, + 'default': 1} + } + + if state == "merged": + discovery_spec["ip_address_list"] = {'type': 'list', 'required': True, + 'elements': 'str'} + discovery_spec["discovery_type"] = {'type': 'str', 'required': True} + + elif state == "deleted": + if self.config[0].get("delete_all") is True: + self.validated_config = [{"delete_all": True}] + self.msg = "Sucessfully collected input for deletion of all the discoveries" + self.log(self.msg, "WARNING") + return self + + # Validate discovery params + valid_discovery, invalid_params = validate_list_of_dicts( + self.config, discovery_spec + ) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params)) + self.log(str(self.msg), "ERROR") + self.status = "failed" + return self + + self.validated_config = valid_discovery + self.msg = "Successfully validated playbook configuration parameters using 'validate_input': {0}".format(str(valid_discovery)) + self.log(str(self.msg), "INFO") + self.status = "success" + return self + + def get_creds_ids_list(self): + """ + Retrieve the list of credentials IDs associated with class instance. + + Returns: + The method returns the list of credentials IDs: + - self.creds_ids_list: The list of credentials IDs associated with + the class instance. + """ + + self.log("Credential Ids list passed is {0}".format(str(self.creds_ids_list)), "INFO") + return self.creds_ids_list + + def get_ccc_global_credentials_v2_info(self): + """ + Retrieve the global credentials information (version 2). + It applies the 'get_all_global_credentials_v2' function and extracts + the IDs of the credentials. If no credentials are found, the + function fails with a message. + + Returns: + This method does not return a value. However, updates the attributes: + - self.creds_ids_list: The list of credentials IDs is extended with + the IDs extracted from the response. + - self.result: A dictionary that is updated with the credentials IDs. + """ + + response = self.dnac_apply['exec']( + family="discovery", + function='get_all_global_credentials_v2', + params=self.validated_config[0].get('headers'), + ) + response = response.get('response') + self.log("The Global credentials response from 'get all global credentials v2' API is {0}".format(str(response)), "DEBUG") + + cli_len_inp = self.validated_config[0].get("cli_cred_len") + if response.get("cliCredential") is None: + msg = 'Not found any CLI credentials to perform discovery' + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + if response.get("snmpV2cRead") is None and response.get("snmpV2cWrite") is None and response.get("snmpV3"): + msg = 'Not found any SNMP credentials to perform discovery' + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + total_cli = len(response.get("cliCredential")) + if total_cli > 5: + if cli_len_inp > 5: + cli_len_inp = 5 + + elif total_cli < 6 and cli_len_inp > total_cli: + cli_len_inp = total_cli + + cli_len = 0 + + for key in response.keys(): + if response[key] is None: + response[key] = [] + if key == "cliCredential": + for element in response.get(key): + while cli_len < cli_len_inp: + self.creds_ids_list.append(element.get('id')) + cli_len += 1 + else: + self.creds_ids_list.extend(element.get('id') for element in response.get(key)) + if not self.creds_ids_list: + msg = 'Not found any credentials to perform discovery' + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + self.result.update(dict(credential_ids=self.creds_ids_list)) + + def get_devices_list_info(self): + """ + Retrieve the list of devices from the validated configuration. + It then updates the result attribute with this list. + + Returns: + - ip_address_list: The list of devices extracted from the + 'validated_config' attribute. + """ + ip_address_list = self.validated_config[0].get('ip_address_list') + self.result.update(dict(devices_info=ip_address_list)) + self.log("Details of the device list passed: {0}".format(str(ip_address_list)), "INFO") + return ip_address_list + + def preprocess_device_discovery(self, ip_address_list=None): + """ + Preprocess the devices' information. Extract the IP addresses from + the list of devices and perform additional processing based on the + 'discovery_type' in the validated configuration. + + Parameters: + - ip_address_list: The list of devices' IP addresses intended for preprocessing. + If not provided, an empty list will be used. + + Returns: + - ip_address_list: It returns IP address list for the API to process. The value passed + for single, CDP, LLDP, CIDR, Range and Multi Range varies depending + on the need. + """ + + if ip_address_list is None: + ip_address_list = [] + discovery_type = self.validated_config[0].get('discovery_type') + self.log("Discovery type passed for the discovery is {0}".format(discovery_type), "INFO") + if discovery_type in ["SINGLE", "CDP", "LLDP"]: + if len(ip_address_list) == 1: + ip_address_list = ip_address_list[0] + else: + self.preprocess_device_discovery_handle_error() + elif discovery_type == "CIDR": + if len(ip_address_list) == 1: + cidr_notation = ip_address_list[0] + if len(cidr_notation.split("/")) == 2: + ip_address_list = cidr_notation + else: + ip_address_list = "{0}/30".format(cidr_notation) + self.log("CIDR notation is being used for discovery and it requires a prefix length to be specified, such as 1.1.1.1/24.\ + As no prefix length was provided, it will default to 30.", "WARNING") + else: + self.preprocess_device_discovery_handle_error() + elif discovery_type == "RANGE": + if len(ip_address_list) == 1: + if len(str(ip_address_list[0]).split("-")) == 2: + ip_address_list = ip_address_list[0] + else: + ip_address_list = "{0}-{1}".format(ip_address_list[0], ip_address_list[0]) + else: + self.preprocess_device_discovery_handle_error() + else: + new_ip_collected = [] + for ip in ip_address_list: + if len(str(ip).split("-")) != 2: + ip_collected = "{0}-{0}".format(ip) + new_ip_collected.append(ip_collected) + else: + new_ip_collected.append(ip) + ip_address_list = ','.join(new_ip_collected) + self.log("Collected IP address/addresses are {0}".format(str(ip_address_list)), "INFO") + return str(ip_address_list) + + def preprocess_device_discovery_handle_error(self): + """ + Method for failing discovery based on the length of list of IP Addresses passed + for performing discovery. + """ + + self.log("IP Address list's length is longer than 1", "ERROR") + self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) + + def http_cred_failure(self, msg=None): + """ + Method for failing discovery if there is any discrepancy in the http credentials + passed by the user + """ + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + def create_params(self, credential_ids=None, ip_address_list=None): + """ + Create a new parameter object based on the validated configuration, + credential IDs, and IP address list. + + Parameters: + - credential_ids: The list of credential IDs to include in the + parameters. If not provided, an empty list is used. + - ip_address_list: The list of IP addresses to include in the + parameters. If not provided, None is used. + + Returns: + - new_object_params: A dictionary containing the newly created + parameters. + """ + + if credential_ids is None: + credential_ids = [] + + http_read_credential = self.validated_config[0].get('http_read_credential') + http_write_credential = self.validated_config[0].get('http_write_credential') + if http_read_credential: + if not (http_read_credential.get('password') and isinstance(http_read_credential.get('password'), str)): + msg = "The password for the HTTP read credential must be of string type." + self.http_cred_failure(msg=msg) + if not (http_read_credential.get('username') and isinstance(http_read_credential.get('username'), str)): + msg = "The username for the HTTP read credential must be of string type." + self.http_cred_failure(msg=msg) + if not (http_read_credential.get('port') and isinstance(http_read_credential.get('port'), int)): + msg = "The port for the HTTP read Credential must be of integer type." + self.http_cred_failure(msg=msg) + if not isinstance(http_read_credential.get('secure'), bool): + msg = "Secure for HTTP read Credential must be of type boolean." + self.http_cred_failure(msg=msg) + + if http_write_credential: + if not (http_write_credential.get('password') and isinstance(http_write_credential.get('password'), str)): + msg = "The password for the HTTP write credential must be of string type." + self.http_cred_failure(msg=msg) + if not (http_write_credential.get('username') and isinstance(http_write_credential.get('username'), str)): + msg = "The username for the HTTP write credential must be of string type." + self.http_cred_failure(msg=msg) + if not (http_write_credential.get('port') and isinstance(http_write_credential.get('port'), int)): + msg = "The port for the HTTP write Credential must be of integer type." + self.http_cred_failure(msg=msg) + if not isinstance(http_write_credential.get('secure'), bool): + msg = "Secure for HTTP write Credential must be of type boolean." + self.http_cred_failure(msg=msg) + + new_object_params = {} + new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') + new_object_params['discoveryType'] = self.validated_config[0].get('discovery_type') + new_object_params['enablePasswordList'] = self.validated_config[0].get( + 'enable_password_list') + new_object_params['globalCredentialIdList'] = credential_ids + new_object_params['httpReadCredential'] = self.validated_config[0].get( + 'http_read_credential') + new_object_params['httpWriteCredential'] = self.validated_config[0].get( + 'http_write_credential') + new_object_params['ipAddressList'] = ip_address_list + new_object_params['ipFilterList'] = self.validated_config[0].get('ip_filter_list') + new_object_params['lldpLevel'] = self.validated_config[0].get('lldp_level') + new_object_params['name'] = self.validated_config[0].get('discovery_name') + new_object_params['netconfPort'] = self.validated_config[0].get('netconf_port') + new_object_params['passwordList'] = self.validated_config[0].get('password_list') + new_object_params['preferredMgmtIPMethod'] = self.validated_config[0].get( + 'preferred_mgmt_ip_method') + new_object_params['protocolOrder'] = self.validated_config[0].get('protocol_order') + new_object_params['retry'] = self.validated_config[0].get('retry') + new_object_params['snmpAuthPassphrase'] = self.validated_config[0].get( + 'snmp_auth_Passphrase') + new_object_params['snmpAuthProtocol'] = self.validated_config[0].get( + 'snmp_auth_protocol') + new_object_params['snmpMode'] = self.validated_config[0].get('snmp_mode') + new_object_params['snmpPrivPassphrase'] = self.validated_config[0].get( + 'snmp_priv_passphrase') + new_object_params['snmpPrivProtocol'] = self.validated_config[0].get( + 'snmp_priv_protocol') + new_object_params['snmpROCommunity'] = self.validated_config[0].get( + 'snmp_ro_community') + new_object_params['snmpROCommunityDesc'] = self.validated_config[0].get( + 'snmp_ro_community_desc') + new_object_params['snmpRWCommunity'] = self.validated_config[0].get( + 'snmp_rw_community') + new_object_params['snmpRWCommunityDesc'] = self.validated_config[0].get( + 'snmp_rw_community_desc') + new_object_params['snmpUserName'] = self.validated_config[0].get( + 'snmp_username') + new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') + new_object_params['timeout'] = self.validated_config[0].get('timeout') + new_object_params['userNameList'] = self.validated_config[0].get('username_list') + self.log("The payload/object created for calling the start discovery API is {0}".format(str(new_object_params)), "INFO") + + return new_object_params + + def create_discovery(self, credential_ids=None, ip_address_list=None): + """ + Start a new discovery process in the Cisco Catalyst Center. It creates the + parameters required for the discovery and then calls the + 'start_discovery' function. The result of the discovery process + is added to the 'result' attribute. + + Parameters: + - credential_ids: The list of credential IDs to include in the + discovery. If not provided, an empty list is used. + - ip_address_list: The list of IP addresses to include in the + discovery. If not provided, None is used. + + Returns: + - task_id: The ID of the task created for the discovery process. + """ + + if credential_ids is None: + credential_ids = [] + + result = self.dnac_apply['exec']( + family="discovery", + function="start_discovery", + params=self.create_params( + credential_ids=credential_ids, ip_address_list=ip_address_list), + op_modifies=True, + ) + + self.log("The response received post discovery creation API called is {0}".format(str(result)), "DEBUG") + + self.result.update(dict(discovery_result=result)) + self.log("Task Id of the API task created is {0}".format(result.response.get('taskId')), "INFO") + return result.response.get('taskId') + + def get_task_status(self, task_id=None): + """ + Monitor the status of a task in the Cisco Catalyst Center. It checks the task + status periodically until the task is no longer 'In Progress'. + If the task encounters an error or fails, it immediately fails the + module and returns False. + + Parameters: + - task_id: The ID of the task to monitor. + + Returns: + - result: True if the task completed successfully, False otherwise. + """ + + result = False + params = dict(task_id=task_id) + while True: + response = self.dnac_apply['exec']( + family="task", + function='get_task_by_id', + params=params, + ) + response = response.response + self.log("Task status for the task id {0} is {1}".format(str(task_id), str(response)), "INFO") + if response.get('isError') or re.search( + 'failed', response.get('progress'), flags=re.IGNORECASE + ): + msg = 'Discovery task with id {0} has not completed - Reason: {1}'.format( + task_id, response.get("failureReason")) + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + return False + + if response.get('progress') != 'In Progress': + result = True + self.log("The Process is completed", "INFO") + break + time.sleep(3) + + self.result.update(dict(discovery_task=response)) + return result + + def lookup_discovery_by_range_via_name(self): + """ + Retrieve a specific discovery by name from a range of + discoveries in the Cisco Catalyst Center. + + Returns: + - discovery: The discovery with the specified name from the range + of discoveries. If no matching discovery is found, it + returns None. + """ + start_index = self.validated_config[0].get("start_index") + records_to_return = self.validated_config[0].get("records_to_return") + + response = {"response": []} + if records_to_return > 500: + num_intervals = records_to_return // 500 + for num in range(0, num_intervals + 1): + params = dict( + start_index=1 + num * 500, + records_to_return=500, + headers=self.validated_config[0].get("headers") + ) + response_part = self.dnac_apply['exec']( + family="discovery", + function='get_discoveries_by_range', + params=params + ) + response["response"].extend(response_part["response"]) + else: + params = dict( + start_index=self.validated_config[0].get("start_index"), + records_to_return=self.validated_config[0].get("records_to_return"), + headers=self.validated_config[0].get("headers"), + ) + + response = self.dnac_apply['exec']( + family="discovery", + function='get_discoveries_by_range', + params=params + ) + self.log("Response of the get discoveries via range API is {0}".format(str(response)), "DEBUG") + + return next( + filter( + lambda x: x['name'] == self.validated_config[0].get('discovery_name'), + response.get("response") + ), None + ) + + def get_discoveries_by_range_until_success(self): + """ + Continuously retrieve a specific discovery by name from a range of + discoveries in the Cisco Catalyst Center until the discovery is complete. + + Returns: + - discovery: The completed discovery with the specified name from + the range of discoveries. If the discovery is not + found or not completed, the function fails the module + and returns None. + """ + + result = False + discovery = self.lookup_discovery_by_range_via_name() + + if not discovery: + msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( + str(self.validated_config[0].get("discovery_name")), str(discovery)) + self.log(msg, "INFO") + self.module.fail_json(msg=msg) + + while True: + discovery = self.lookup_discovery_by_range_via_name() + if discovery.get('discoveryCondition') == 'Complete': + result = True + break + + time.sleep(3) + + if not result: + msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( + str(self.validated_config[0].get("discovery_name")), str(discovery)) + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + self.result.update(dict(discovery_range=discovery)) + return discovery + + def get_discovery_device_info(self, discovery_id=None, task_id=None): + """ + Retrieve the information of devices discovered by a specific discovery + process in the Cisco Catalyst Center. It checks the reachability status of the + devices periodically until all devices are reachable or until a + maximum of 3 attempts. + + Parameters: + - discovery_id: ID of the discovery process to retrieve devices from. + - task_id: ID of the task associated with the discovery process. + + Returns: + - result: True if all devices are reachable, False otherwise. + """ + + params = dict( + id=discovery_id, + task_id=task_id, + headers=self.validated_config[0].get("headers"), + ) + result = False + count = 0 + while True: + response = self.dnac_apply['exec']( + family="discovery", + function='get_discovered_network_devices_by_discovery_id', + params=params, + ) + devices = response.response + + self.log("Retrieved device details using the API 'get_discovered_network_devices_by_discovery_id': {0}".format(str(devices)), "DEBUG") + if all(res.get('reachabilityStatus') == 'Success' for res in devices): + result = True + break + + count += 1 + if count == 3: + break + + time.sleep(3) + + if not result: + msg = 'Discovery network device with id {0} has not completed'.format(discovery_id) + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + self.log('Discovery network device with id {0} got completed'.format(discovery_id), "INFO") + self.result.update(dict(discovery_device_info=devices)) + return result + + def get_exist_discovery(self): + """ + Retrieve an existing discovery by its name from a range of discoveries. + + Returns: + - discovery: The discovery with the specified name from the range of + discoveries. If no matching discovery is found, it + returns None and updates the 'exist_discovery' entry in + the result dictionary to None. + """ + discovery = self.lookup_discovery_by_range_via_name() + if not discovery: + self.result.update(dict(exist_discovery=discovery)) + return None + + have = dict(exist_discovery=discovery) + self.have = have + self.result.update(dict(exist_discovery=discovery)) + return discovery + + def delete_exist_discovery(self, params): + """ + Delete an existing discovery in the Cisco Catalyst Center by its ID. + + Parameters: + - params: A dictionary containing the parameters for the delete + operation, including the ID of the discovery to delete. + + Returns: + - task_id: The ID of the task created for the delete operation. + """ + + response = self.dnac_apply['exec']( + family="discovery", + function="delete_discovery_by_id", + params=params, + ) + + self.log("Response collected from API 'delete_discovery_by_id': {0}".format(str(response)), "DEBUG") + self.result.update(dict(delete_discovery=response)) + self.log("Task Id of the deletion task is {0}".format(response.response.get('taskId')), "INFO") + return response.response.get('taskId') + + def get_diff_merged(self): + """ + Retrieve the information of devices discovered by a specific discovery + process in the Cisco Catalyst Center, delete existing discoveries if they exist, + and create a new discovery. The function also updates various + attributes of the class instance. + + Returns: + - self: The instance of the class with updated attributes. + """ + + self.get_ccc_global_credentials_v2_info() + devices_list_info = self.get_devices_list_info() + ip_address_list = self.preprocess_device_discovery(devices_list_info) + exist_discovery = self.get_exist_discovery() + if exist_discovery: + params = dict(id=exist_discovery.get('id')) + discovery_task_id = self.delete_exist_discovery(params=params) + complete_discovery = self.get_task_status(task_id=discovery_task_id) + + discovery_task_id = self.create_discovery( + credential_ids=self.get_creds_ids_list(), + ip_address_list=ip_address_list) + complete_discovery = self.get_task_status(task_id=discovery_task_id) + discovery_task_info = self.get_discoveries_by_range_until_success() + result = self.get_discovery_device_info(discovery_id=discovery_task_info.get('id')) + self.result["changed"] = True + self.result['msg'] = "Discovery Created Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = discovery_task_id + self.result.update(dict(msg='Discovery Created Successfully')) + self.log(self.result['msg'], "INFO") + return self + + def get_diff_deleted(self): + """ + Delete an existing discovery in the Cisco Catalyst Center by its name, and + updates various attributes of the class instance. If no + discovery with the specified name is found, the function + updates the 'msg' attribute with an appropriate message. + + Returns: + - self: The instance of the class with updated attributes. + """ + + if self.validated_config[0].get("delete_all"): + count_discoveries = self.dnac_apply['exec']( + family="discovery", + function="get_count_of_all_discovery_jobs", + ) + if count_discoveries.get("response") == 0: + msg = "There are no discoveries present in the Discovery Dashboard for deletion" + self.result['msg'] = msg + self.log(msg, "WARNING") + self.result['response'] = self.validated_config[0] + return self + + delete_all_response = self.dnac_apply['exec']( + family="discovery", + function="delete_all_discovery", + ) + discovery_task_id = delete_all_response.get('response').get('taskId') + self.result["changed"] = True + self.result['msg'] = "All of the Discoveries Deleted Successfully" + self.result['diff'] = self.validated_config + + else: + exist_discovery = self.get_exist_discovery() + if not exist_discovery: + self.result['msg'] = "Discovery {0} Not Found".format( + self.validated_config[0].get("discovery_name")) + self.log(self.result['msg'], "ERROR") + return self + + params = dict(id=exist_discovery.get('id')) + discovery_task_id = self.delete_exist_discovery(params=params) + complete_discovery = self.get_task_status(task_id=discovery_task_id) + self.result["changed"] = True + self.result['msg'] = "Successfully deleted discovery" + self.result['diff'] = self.validated_config + self.result['response'] = discovery_task_id + + self.log(self.result['msg'], "INFO") + return self + + def verify_diff_merged(self, config): + """ + Verify the merged status(Creation/Updation) of Discovery in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the merged status of a configuration in Cisco Catalyst Center by + retrieving the current state (have) and desired state (want) of the configuration, + logs the states, and validates whether the specified device(s) exists in the DNA + Center configuration's Discovery Database. + """ + + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(config)), "INFO") + # Code to validate Cisco Catalyst Center config for merged state + discovery_task_info = self.get_discoveries_by_range_until_success() + discovery_id = discovery_task_info.get('id') + params = dict( + id=discovery_id + ) + response = self.dnac_apply['exec']( + family="discovery", + function='get_discovery_by_id', + params=params + ) + discovery_name = config.get('discovery_name') + if response: + self.log("Requested Discovery with name {0} is completed".format(discovery_name), "INFO") + + else: + self.log("Requested Discovery with name {0} is not completed".format(discovery_name), "WARNING") + self.status = "success" + + return self + + def verify_diff_deleted(self, config): + """ + Verify the deletion status of Discovery in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified discovery(s) exists in the Cisco Catalyst Center configuration's + Discovery Database. + """ + + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(config)), "INFO") + # Code to validate Cisco Catalyst Center config for deleted state + discovery_task_info = self.lookup_discovery_by_range_via_name() + discovery_name = config.get('discovery_name') + if discovery_task_info: + self.log("Requested Discovery with name {0} is present".format(discovery_name), "WARNING") + + else: + self.log("Requested Discovery with name {0} is not present and deleted".format(discovery_name), "INFO") + self.status = "success" + + return self + + +def main(): + """ main entry point for module execution + """ + + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {"type": 'bool', "default": False}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } + + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + + ccc_discovery = Discovery(module) + config_verify = ccc_discovery.params.get("config_verify") + + state = ccc_discovery.params.get("state") + if state not in ccc_discovery.supported_states: + ccc_discovery.status = "invalid" + ccc_discovery.msg = "State {0} is invalid".format(state) + ccc_discovery.check_return_status() + + ccc_discovery.validate_input(state=state).check_return_status() + for config in ccc_discovery.validated_config: + ccc_discovery.reset_values() + ccc_discovery.get_diff_state_apply[state]().check_return_status() + if config_verify: + ccc_discovery.verify_diff_state_apply[state](config).check_return_status() + + module.exit_json(**ccc_discovery.result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py new file mode 100644 index 0000000000..4e662433e5 --- /dev/null +++ b/plugins/modules/pnp_workflow_manager.py @@ -0,0 +1,1142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ("Abinash Mishra, MadhanSankaranarayanan, Rishita Chowdhary") + +DOCUMENTATION = r""" +--- +module: pnp_workflow_manager +short_description: Resource module for Site and PnP related functions +description: +- Manage operations add device, claim device and unclaim device of Onboarding Configuration(PnP) resource +- API to add device to pnp inventory and claim it to a site. +- API to delete device from the pnp inventory. +- API to reset the device from errored state. +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.workflow_manager_params +author: Abinash Mishra (@abimishr) + Madhan Sankaranarayanan (@madhansansel) + Rishita Chowdhary (@rishitachowdhary) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. + type: bool + default: False + state: + description: The state of Cisco Catalyst Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of device being managed. + type: list + elements: dict + required: true + suboptions: + template_name: + description: Name of template to be configured on the device. + type: str + template_params: + description: Parameter values for the parameterised templates. + Each varibale has a value that needs to be passed as key-value pair + in the dictionary. We can pass values as variable_name:variable_value. + type: dict + image_name: + description: Name of image to be configured on the device + type: str + golden_image: + description: Is the image to be condifgured tagged as golden image + type: bool + site_name: + description: Name of the site for which device will be claimed. + type: str + project_name: + description: Name of the project under which the template is present + type: str + default: Onboarding Configuration + pnp_type: + description: Device type of the Pnp device (Default/catalyst_wlc/access_point/stack_switch) + type: str + default: Default + static_ip: + description: Management IP address of the Wireless Controller + type: str + subnet_mask: + description: Subnet Mask of the Management IP address of the Wireless Controller + type: str + gateway: + description: Gateway IP address of the Wireless Controller for getting pinged + type: str + vlan_id: + description: Vlan Id allocated for claimimg of Wireless Controller + type: str + ip_interface_name: + description: Name of the Interface used for Pnp by the Wireless Controller + type: str + rf_profile: + description: Radio frequecy profile of the AP being claimed (HIGH/LOW/TYPICAL) + type: str + device_info: + description: Pnp Device's device_info. This is mainly for adding the devices that are + not a part of the PnP database. For single addition the length of the list must be equal to one. + Followed by single addition a device can be claimed as well if site name is provided. + For Bulk Import of devices the size of the list must be greater than 1 and can be only used for adding. + For claiming the devices please use separate tasks or configs in the case of bulk import. + type: list + required: true + elements: dict + suboptions: + hostname: + description: Pnp Device's hostname that we want to keep post claiming. Hostname can only + be changed during claiming not bulk adding/ single adding + type: str + state: + description: Pnp Device's onbording state (Unclaimed/Claimed/Provisioned). + type: str + pid: + description: Pnp Device's pid. + type: str + serial_number: + description: Pnp Device's serial_number. + type: str + is_sudi_required: + description: Sudi Authentication requiremnet's flag. + type: bool + +requirements: +- dnacentersdk == 2.6.10 +- python >= 3.5 +notes: + - SDK Method used are + device_onboarding_pnp.DeviceOnboardingPnp.add_device, + device_onboarding_pnp.DeviceOnboardingPnp.get_device_list, + device_onboarding_pnp.DeviceOnboardingPnp.claim_a_device_to_a_site, + device_onboarding_pnp.DeviceOnboardingPnp.delete_device_by_id_from_pnp, + device_onboarding_pnp.DeviceOnboardingPnp.get_device_count, + device_onboarding_pnp.DeviceOnboardingPnp.get_device_by_id, + device_onboarding_pnp.DeviceOnboardingPnp.update_device, + sites.Sites.get_site, + software_image_management_swim.SoftwareImageManagementSwim.get_software_image_details, + configuration_templates.ConfigurationTemplates.gets_the_templates_available + + - Paths used are + post /dna/intent/api/v1/onboarding/pnp-device + post /dna/intent/api/v1/onboarding/pnp-device/site-claim + post /dna/intent/api/v1/onboarding/pnp-device/{id} + get /dna/intent/api/v1/onboarding/pnp-device/count + get /dna/intent/api/v1/onboarding/pnp-device + put /onboarding/pnp-device/${id} + get /dna/intent/api/v1/site + get /dna/intent/api/v1/image/importation + get /dna/intent/api/v1/template-programmer/template + +""" + +EXAMPLES = r""" +- name: Add a new device and claim the device + cisco.dnac.pnp_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: True + state: merged + config_verify: True + config: + - template_name: string + image_name: string + golden_image: bool + site_name: string + project_name: string + pnp_type: string + static_ip: string + subnet_mask: string + gateway: string + vlan_id: string + ip_interface_name: string + rf_profile: string + device_info: + - hostname: string + state: string + pid: string + serial_number: string + add_device_method: string + is_sudi_required: string +""" + +RETURN = r""" +#Case_1: When the device is claimed successfully. +response_1: + description: A dictionary with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": + { + "response": String, + "version": String + }, + "msg": String + } + +#Case_2: Given site/image/template/project not found or Device is not found for deletion +response_2: + description: A list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: list + sample: > + { + "response": [], + "msg": String + } + +#Case_3: Error while deleting/claiming a device +response_3: + description: A string with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": String, + "msg": String + } +""" +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, + get_dict_result +) + + +class PnP(DnacBase): + def __init__(self, module): + super().__init__(module) + + def validate_input(self): + """ + Validate the fields provided in the playbook. Checks the + configuration provided in the playbook against a predefined + specification to ensure it adheres to the expected structure + and data types. + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of the + 'config' parameter. + Example: + To use this method, create an instance of the class and call + 'validate_input' on it.If the validation succeeds, 'self.status' + will be 'success'and 'self.validated_config' will contain the + validated configuration. If it fails, 'self.status' will be + 'failed', and 'self.msg' will describe the validation issues. + """ + + if not self.config: + self.msg = "config not available in playbook for validation" + self.status = "success" + return self + + pnp_spec = { + 'template_name': {'type': 'str', 'required': False}, + 'template_params': {'type': 'dict', 'required': False}, + 'project_name': {'type': 'str', 'required': False, + 'default': 'Onboarding Configuration'}, + 'site_name': {'type': 'str', 'required': False}, + 'image_name': {'type': 'str', 'required': False}, + 'golden_image': {'type': 'bool', 'required': False}, + 'device_info': {'type': 'list', 'required': True, + 'elements': 'dict'}, + 'pnp_type': {'type': 'str', 'required': False, 'default': 'Default'}, + "rf_profile": {'type': 'str', 'required': False}, + "static_ip": {'type': 'str', 'required': False}, + "subnet_mask": {'type': 'str', 'required': False}, + "gateway": {'type': 'str', 'required': False}, + "vlan_id": {'type': 'str', 'required': False}, + "ip_interface_name": {'type': 'str', 'required': False}, + "sensorProfile": {'type': 'str', 'required': False} + } + + # Validate pnp params + valid_pnp, invalid_params = validate_list_of_dicts( + self.config, pnp_spec + ) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params)) + self.log(str(self.msg), "ERROR") + self.status = "failed" + return self + self.validated_config = valid_pnp + self.msg = "Successfully validated playbook config params: {0}".format(str(valid_pnp)) + self.log(str(self.msg), "INFO") + self.status = "success" + + return self + + def get_site_details(self): + """ + Check whether the site exists or not, along with side id + + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - site_exits: A boolean value indicating the existence of the site. + - site_id: The Id of the site i.e. required to claim device to site. + Example: + Post creation of the validated input, we this method gets the + site_id and checks whether the site exists or not + """ + + site_exists = False + site_id = None + response = None + + try: + response = self.dnac_apply['exec']( + family="sites", + function='get_site', + params={"name": self.want.get("site_name")}, + ) + except Exception: + self.log("Exception occurred as site \ + '{0}' was not found".format(self.want.get("site_name")), "CRITICAL") + self.module.fail_json(msg="Site not found", response=[]) + + if response: + self.log("Received site details \ + for '{0}': {1}".format(self.want.get("site_name"), str(response)), "DEBUG") + site = response.get("response") + if len(site) == 1: + site_id = site[0].get("id") + site_exists = True + self.log("Site Name: {1}, Site ID: {0}".format(site_id, self.want.get("site_name")), "INFO") + + return (site_exists, site_id) + + def get_site_type(self): + """ + Fetches the type of site + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - site_type: A string indicating the type of the + site (area/building/floor). + Example: + Post creation of the validated input, we this method gets the + type of the site. + """ + + try: + response = self.dnac_apply['exec']( + family="sites", + function='get_site', + params={"name": self.want.get("site_name")}, + ) + except Exception: + self.log("Exception occurred as \ + site '{0}' was not found".format(self.want.get("site_name")), "CRITICAL") + self.module.fail_json(msg="Site not found", response=[]) + + if response: + self.log("Received site details\ + for '{0}': {1}".format(self.want.get("site_name"), str(response)), "DEBUG") + site = response.get("response") + site_additional_info = site[0].get("additionalInfo") + for item in site_additional_info: + if item["nameSpace"] == "Location": + site_type = item.get("attributes").get("type") + self.log("Site type for site name '{1}' : {0}".format(site_type, self.want.get("site_name")), "INFO") + + return site_type + + def get_pnp_params(self, params): + """ + Store pnp parameters from the playbook for pnp processing in Cisco Catalyst Center. + + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + - params: The validated params passed from the playbook. + Returns: + The method returns an instance of the class with updated attributes: + - pnp_params: A dictionary containing all the values indicating + the type of the site (area/building/floor). + Example: + Post creation of the validated input, it fetches the required paramters + and stores it for further processing and calling the parameters in + other APIs. + """ + + params_list = params["device_info"] + device_info_list = [] + for param in params_list: + device_dict = {} + param["serialNumber"] = param.pop("serial_number") + if "is_sudi_required" in param: + param["isSudiRequired"] = param.pop("is_sudi_required") + device_dict["deviceInfo"] = param + device_info_list.append(device_dict) + + self.log("PnP paramters passed are {0}".format(str(params_list)), "INFO") + return device_info_list + + def get_image_params(self, params): + """ + Get image name and the confirmation whether it's tagged golden or not + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + - params: The validated params passed from the playbook. + Returns: + The method returns an instance of the class with updated attributes: + - image_params: A dictionary containing all the values indicating + name of the image and its golden image status. + Example: + Post creation of the validated input, it fetches the required + paramters and stores it for further processing and calling the + parameters in other APIs. + """ + + image_params = { + 'image_name': params.get('image_name'), + 'is_tagged_golden': params.get('golden_image') + } + + self.log("Image details are {0}".format(str(image_params)), "INFO") + return image_params + + def get_claim_params(self): + """ + Get the paramters needed for claiming the device to site. + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - claim_params: A dictionary needed for calling the POST call + for claim a device to a site API. + Example: + The stored dictionary can be used to call the API claim a device + to a site via SDK + """ + + imageinfo = { + 'imageId': self.have.get('image_id') + } + + configinfo = { + 'configId': self.have.get('template_id'), + 'configParameters': [ + { + 'key': '', + 'value': '' + } + ] + } + + if configinfo["configId"] and self.validated_config[0]["template_params"]: + if isinstance(self.validated_config[0]["template_params"], dict): + if len(self.validated_config[0]["template_params"]) > 0: + configinfo["configParameters"] = [] + for key, value in self.validated_config[0]["template_params"].items(): + config_dict = { + 'key': key, + 'value': value + } + configinfo["configParameters"].append(config_dict) + + claim_params = { + 'deviceId': self.have.get('device_id'), + 'siteId': self.have.get('site_id'), + 'type': self.want.get('pnp_type'), + 'hostname': self.want.get('hostname'), + 'imageInfo': imageinfo, + 'configInfo': configinfo, + } + + if claim_params["type"] == "catalyst_wlc": + claim_params["type"] = "CatalystWLC" + claim_params["staticIP"] = self.validated_config[0]['static_ip'] + claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] + claim_params["gateway"] = self.validated_config[0]['gateway'] + claim_params["vlanId"] = str(self.validated_config[0]['vlan_id']) + claim_params["ipInterfaceName"] = self.validated_config[0]['ip_interface_name'] + + if claim_params["type"] == "access_point": + claim_params["type"] = "AccessPoint" + claim_params["rfProfile"] = self.validated_config[0]["rf_profile"] + + self.log("Paramters used for claiming are {0}".format(str(claim_params)), "INFO") + return claim_params + + def get_reset_params(self): + """ + Get the paramters needed for resetting the device in an errored state. + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - reset_params: A dictionary needed for calling the PUT call + for update device details API. + Example: + The stored dictionary can be used to call the API update device details + """ + + reset_params = { + "deviceResetList": [ + { + "configList": [ + { + "configId": self.have.get('template_id'), + "configParameters": [ + { + "key": "", + "value": "" + } + ] + } + ], + "deviceId": self.have.get('device_id'), + "licenseLevel": "", + "licenseType": "", + "topOfStackSerialNumber": "" + } + ] + } + + self.log("Paramters used for resetting from errored state:{0}".format(str(reset_params)), "INFO") + return reset_params + + def get_have(self): + """ + Get the current image, template and site details from the Cisco Catalyst Center. + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - self.image_response: A list of image passed by the user + - self.template_list: A list of template under project + - self.device_response: Gets the device_id and stores it + Example: + Stored paramters are used to call the APIs to get the current image, + template and site details to call the API for various types of devices + """ + have = {} + + # Claiming is only allowed for single addition of devices + if len(self.want.get('pnp_params')) == 1: + # check if given device exists in pnp inventory, store device Id + device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": self.want.get("serial_number")} + ) + self.log("Device details for the device with serial \ + number '{0}': {1}".format(self.want.get("serial_number"), str(device_response)), "DEBUG") + + if not (device_response and (len(device_response) == 1)): + self.log("Device with with serial number {0} is not found in the inventory".format(self.want.get("serial_number")), "WARNING") + self.msg = "Adding the device to database" + self.status = "success" + self.have = have + have["device_found"] = False + return self + + have["device_found"] = True + have["device_id"] = device_response[0].get("id") + self.log("Device Id: " + str(have["device_id"])) + + if self.params.get("state") == "merged": + # check if given image exists, if exists store image_id + image_response = self.dnac_apply['exec']( + family="software_image_management_swim", + function='get_software_image_details', + params=self.want.get("image_params"), + ) + image_list = image_response.get("response") + self.log("Image details obtained from the API 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") + + # check if project has templates or not + template_list = self.dnac_apply['exec']( + family="configuration_templates", + function='gets_the_templates_available', + params={"project_names": self.want.get("project_name")}, + ) + self.log("List of templates under the project '{0}': {1}".format(self.want.get("project_name"), str(template_list)), "DEBUG") + + dev_details_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="get_device_by_id", + params={"id": device_response[0].get("id")} + ) + self.log("Device details retrieved after calling the 'get_device_by_id' API: {0}".format(str(dev_details_response)), "DEBUG") + install_mode = dev_details_response.get("deviceInfo").get("mode") + self.log("Installation mode of the device with the serial no. '{0}':{1}".format(self.want.get("serial_number"), install_mode), "INFO") + + # check if given site exits, if exists store current site info + site_exists = False + if not isinstance(self.want.get("site_name"), str) and \ + not self.want.get('pnp_params')[0].get('deviceInfo'): + self.msg = "The site name must be a string" + self.log(str(self.msg), "ERROR") + self.status = "failed" + return self + + site_name = self.want.get("site_name") + (site_exists, site_id) = self.get_site_details() + + if site_exists: + have["site_id"] = site_id + self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") + if self.want.get("pnp_type") == "access_point": + if self.get_site_type() != "floor": + self.msg = "The site type must be specified as 'floor'\ + for claiming an AP" + self.log(str(self.msg), "ERROR") + self.status = "failed" + return self + + if len(image_list) == 1: + if install_mode != "INSTALL": + self.msg = "Installation mode must be in \ + INSTALL mode to upgrade the image. Current mode is\ + {0}".format(install_mode) + self.log(str(self.msg), "CRITICAL") + self.status = "failed" + return self + + have["image_id"] = image_list[0].get("imageUuid") + self.log("Image ID for the image '{0}': {1}".format(self.want.get('image_params').get('image_name'), str(have["image_id"])), "INFO") + + template_name = self.want.get("template_name") + if template_name: + if not (template_list and isinstance(template_list, list)): + self.msg = "Either project not found \ + or it is Empty" + self.log(self.msg, "CRITICAL") + self.status = "failed" + return self + + template_details = get_dict_result(template_list, 'name', template_name) + if template_details: + have["template_id"] = template_details.get("templateId") + else: + self.msg = "Template '{0}' is not found.".format(template_name) + self.log(self.msg, "CRITICAL") + self.status = "failed" + return self + + else: + if not self.want.get('pnp_params')[0].get('deviceInfo'): + self.msg = "Either Site Name or Device details must be added" + self.log(self.msg, "ERROR") + self.status = "failed" + return self + + self.msg = "Successfully collected all project and template \ + parameters from Cisco Catalyst Center for comparison" + self.log(self.msg, "INFO") + self.status = "success" + self.have = have + return self + + def get_want(self, config): + """ + Get all the image, template and site and pnp related + information from playbook that is needed to be created in Cisco Catalyst Center. + + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + - config: validated config passed from the playbook + Returns: + The method returns an instance of the class with updated attributes: + - self.want: A dictionary of paramters obtained from the playbook. + - self.msg: A message indicating all the paramters from the playbook + are collected. + - self.status: Success. + Example: + It stores all the paramters passed from the playbook for further + processing before calling the APIs + """ + + self.want = { + 'image_params': self.get_image_params(config), + 'pnp_params': self.get_pnp_params(config), + 'pnp_type': config.get('pnp_type'), + 'site_name': config.get('site_name'), + 'project_name': config.get('project_name'), + 'template_name': config.get('template_name') + } + if len(self.want.get('pnp_params')) == 1: + self.want["serial_number"] = ( + self.want['pnp_params'][0]["deviceInfo"]. + get("serialNumber") + ) + self.want["hostname"] = ( + self.want['pnp_params'][0]["deviceInfo"]. + get("hostname") + ) + + if self.want["pnp_type"] == "catalyst_wlc": + self.want["static_ip"] = config.get('static_ip') + self.want["subnet_mask"] = config.get('subnet_mask') + self.want["gateway"] = config.get('gateway') + self.want["vlan_id"] = config.get('vlan_id') + self.want["ip_interface_name"] = config.get('ip_interface_name') + + elif self.want["pnp_type"] == "access_point": + self.want["rf_profile"] = config.get("rf_profile") + self.msg = "Successfully collected all parameters from playbook " + \ + "for comparison" + self.log(self.msg, "INFO") + self.status = "success" + + return self + + def get_diff_merged(self): + """ + If given device doesnot exist + then add it to pnp database and get the device id + Args: + self: An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + object: An instance of the class with updated results and status + based on the processing of differences. Based on the length of devices passed + it adds/claims or does both. + Description: + The function processes the differences and, depending on the + changes required, it may add, update,or resynchronize devices in + Cisco Catalyst Center. The updated results and status are stored in the + class instance for further use. + """ + + if not isinstance(self.want.get("pnp_params"), list): + self.msg = "Device Info must be passed as a list" + self.log(self.msg, "ERROR") + self.status = "failed" + return self + + if len(self.want.get("pnp_params")) > 1: + devices_added = [] + for device in self.want.get("pnp_params"): + multi_device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + self.log("Device details for serial number {0} \ + obtained from the API 'get_device_list': {1}".format(device["deviceInfo"]["serialNumber"], str(multi_device_response)), "DEBUG") + if (multi_device_response and (len(multi_device_response) == 1)): + devices_added.append(device) + self.log("Details of the added device:{0}".format(str(device)), "INFO") + if (len(self.want.get("pnp_params")) - len(devices_added)) == 0: + self.result['response'] = [] + self.result['msg'] = "Devices are already added" + self.log(self.result['msg'], "WARNING") + return self + + bulk_list = [ + device + for device in self.want.get("pnp_params") + if device not in devices_added + ] + bulk_params = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="import_devices_in_bulk", + params={"payload": bulk_list}, + op_modifies=True, + ) + self.log("Response from API 'import_devices_in_bulk' for imported devices: {0}".format(bulk_params), "DEBUG") + if len(bulk_params.get("successList")) > 0: + self.result['msg'] = "{0} device(s) imported successfully".format( + len(bulk_params.get("successList"))) + self.log(self.result['msg'], "INFO") + self.result['response'] = bulk_params + self.result['diff'] = self.validated_config + self.result['changed'] = True + return self + + self.msg = "Bulk import failed" + self.log(self.msg, "CRITICAL") + self.status = "failed" + return self + + provisioned_count_params = { + "serial_number": self.want.get("serial_number"), + "state": "Provisioned" + } + + planned_count_params = { + "serial_number": self.want.get("serial_number"), + "state": "Planned" + } + + if not self.have.get("device_found"): + if not self.want['pnp_params']: + self.msg = "Device needs to be added before claiming. Please add device_info" + self.log(self.msg, "ERROR") + self.status = "failed" + return self + + if not self.want["site_name"]: + self.log("Adding device to pnp database", "INFO") + dev_add_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="add_device", + params=self.want.get('pnp_params')[0], + op_modifies=True, + ) + + self.have["deviceInfo"] = dev_add_response.get("deviceInfo") + self.log("Response from API 'add device' for a single device addition: {0}".format(str(dev_add_response)), "DEBUG") + if self.have["deviceInfo"]: + self.result['msg'] = "Only Device Added Successfully" + self.log(self.result['msg'], "INFO") + self.result['response'] = dev_add_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + + else: + self.msg = "Device Addition Failed" + self.log(self.result['msg'], "CRITICAL") + self.status = "failed" + + return self + + else: + self.log("Adding device to pnp database") + dev_add_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="add_device", + params=self.want.get("pnp_params")[0], + op_modifies=True, + ) + self.have["deviceInfo"] = dev_add_response.get("deviceInfo") + self.log("Response from API 'add device' for single device addition: {0}".format(str(dev_add_response)), "DEBUG") + claim_params = self.get_claim_params() + claim_params["deviceId"] = dev_add_response.get("id") + claim_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='claim_a_device_to_a_site', + op_modifies=True, + params=claim_params, + ) + + self.log("Response from API 'claim a device to a site' for a single claiming: {0}".format(str(dev_add_response)), "DEBUG") + if claim_response.get("response") == "Device Claimed" \ + and self.have["deviceInfo"]: + self.result['msg'] = "Device Added and Claimed Successfully" + self.log(self.result['msg'], "INFO") + self.result['response'] = claim_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + + else: + self.msg = "Device Claim Failed" + self.log(self.result['msg'], "CRITICAL") + self.status = "failed" + + return self + + prov_dev_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_count', + op_modifies=True, + params=provisioned_count_params, + ) + self.log("Response from 'get device count' API for provisioned devices: {0}".format(str(prov_dev_response)), "DEBUG") + + plan_dev_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_count', + op_modifies=True, + params=planned_count_params, + ) + self.log("Response from 'get_device_count' API for devices in planned state: {0}".format(str(plan_dev_response)), "DEBUG") + + dev_details_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="get_device_by_id", + params={"id": self.have["device_id"]} + ) + self.log("Response from 'get_device_by_id' API for device details: {0}".format(str(dev_details_response)), "DEBUG") + + pnp_state = dev_details_response.get("deviceInfo").get("state") + self.log("PnP state of the device: {0}".format(pnp_state), "INFO") + + if not self.want["site_name"]: + self.result['response'] = self.have.get("device_found") + self.result['msg'] = "Device is already added" + self.log(self.result['msg'], "WARNING") + return self + + update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} + update_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="update_device", + params={"id": self.have["device_id"], + "payload": update_payload}, + op_modifies=True, + ) + self.log("Response from 'update_device' API for device's config update: {0}".format(str(update_response)), "DEBUG") + + if pnp_state == "Error": + reset_paramters = self.get_reset_params() + reset_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="update_device", + params={"payload": reset_paramters}, + op_modifies=True, + ) + self.log("Response from 'update_device' API for errored state resolution: {0}".format(str(reset_response)), "DEBUG") + self.result['msg'] = "Device reset done Successfully" + self.log(self.result['msg'], "INFO") + self.result['response'] = reset_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + + if not ( + prov_dev_response.get("response") == 0 and + plan_dev_response.get("response") == 0 and + pnp_state == "Unclaimed" + ): + self.result['response'] = self.have.get("device_found") + self.result['msg'] = "Device is already claimed" + self.log(self.result['msg'], "WARNING") + if update_response.get("deviceInfo"): + self.result['changed'] = True + return self + + claim_params = self.get_claim_params() + self.log("Parameters for claiming the device: {0}".format(str(claim_params)), "DEBUG") + + claim_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='claim_a_device_to_a_site', + op_modifies=True, + params=claim_params, + ) + self.log("Response from 'claim_a_device_to_a_site' API for claiming: {0}".format(str(claim_response)), "DEBUG") + if claim_response.get("response") == "Device Claimed": + self.result['msg'] = "Only Device Claimed Successfully" + self.log(self.result['msg'], "INFO") + self.result['response'] = claim_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + + return self + + def get_diff_deleted(self): + """ + If the given device is added to pnp database + and is in unclaimed or failed state delete the + given device + Args: + self: An instance of a class used for interacting with Cisco Catalyst Center. + Here we pass a list of device info to be deleted + Returns: + self: An instance of the class with updated results and status based on + the deletion operation. It tells us the number of devices deleted if any of the devices + get deleted + Description: + This function is responsible for removing devices from the Cisco Catalyst Center PnP GUI and + pass new changes if devices are already deleted. + """ + devices_deleted = [] + devices_to_delete = self.want.get("pnp_params")[:] + for device in devices_to_delete: + multi_device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + self.log("Response from 'get_device_list' API for claiming: {0}".format(str(multi_device_response)), "DEBUG") + if multi_device_response and len(multi_device_response) == 1: + device_id = multi_device_response[0].get("id") + + response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="delete_device_by_id_from_pnp", + op_modifies=True, + params={"id": device_id}, + ) + self.log("Device details for the deleted device with \ + serial number '{0}': {1}".format(device["deviceInfo"]["serialNumber"], str(response)), "DEBUG") + if response.get("deviceInfo", {}).get("state") == "Deleted": + devices_deleted.append(device["deviceInfo"]["serialNumber"]) + self.want.get("pnp_params").remove(device) + else: + self.result['response'] = response + self.result['msg'] = "Error while deleting the device" + self.log(self.result['msg'], "CRITICAL") + + if len(devices_deleted) > 0: + self.result['changed'] = True + self.result['response'] = devices_deleted + self.result['diff'] = self.want.get("pnp_params") + self.result['msg'] = "{0} Device(s) Deleted Successfully".format(len(devices_deleted)) + self.log(self.result['msg'], "INFO") + else: + self.result['msg'] = "Device(s) Not Found" + self.log(self.result['msg'], "WARNING") + self.result['response'] = devices_deleted + + return self + + def verify_diff_merged(self, config): + """ + Verify the merged status(Creation/Updation) of PnP configuration in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the merged status of a configuration in Cisco Catalyst Center by + retrieving the current state (have) and desired state (want) of the configuration, + logs the states, and validates whether the specified device(s) exists in the DNA + Center configuration's PnP Database. + """ + + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(config)), "INFO") + # Code to validate Cisco Catalyst Center config for merged state + for device in self.want.get("pnp_params"): + device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + if (device_response and (len(device_response) == 1)): + msg = ( + "Requested Device with Serial No. {0} is " + "present in Cisco Catalyst Center and" + " addition verified.".format(device["deviceInfo"]["serialNumber"])) + self.log(msg, "INFO") + + else: + msg = ( + "Requested Device with Serial No. {0} is " + "not present in Cisco Catalyst Center" + "Center".format(device["deviceInfo"]["serialNumber"])) + self.log(msg, "WARNING") + + self.status = "success" + return self + + def verify_diff_deleted(self, config): + """ + Verify the deletion status of PnP configuration in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified device(s) exists in the Cisco Catalyst Center configuration's + PnP Database. + """ + + self.log("Current State (have): {0}".format(str(self.have)), "INFO") + self.log("Desired State (want): {0}".format(str(config)), "INFO") + # Code to validate Cisco Catalyst Center config for deleted state + for device in self.want.get("pnp_params"): + device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + if not (device_response and (len(device_response) == 1)): + msg = ( + "Requested Device with Serial No. {0} is " + "not present in the Cisco DNA" + "Center.".format(device["deviceInfo"]["serialNumber"])) + self.log(msg, "INFO") + + else: + msg = ( + "Requested Device with Serial No. {0} is " + "present in Cisco Catalyst Center".format(device["deviceInfo"]["serialNumber"])) + self.log(msg, "WARNING") + + self.status = "success" + return self + + +def main(): + """ + main entry point for module execution + """ + + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {"type": 'bool', "default": False}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } + + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + ccc_pnp = PnP(module) + + state = ccc_pnp.params.get("state") + if state not in ccc_pnp.supported_states: + ccc_pnp.status = "invalid" + ccc_pnp.msg = "State {0} is invalid".format(state) + ccc_pnp.check_return_status() + + ccc_pnp.validate_input().check_return_status() + config_verify = ccc_pnp.params.get("config_verify") + + for config in ccc_pnp.validated_config: + ccc_pnp.reset_values() + ccc_pnp.get_want(config).check_return_status() + ccc_pnp.get_have().check_return_status() + ccc_pnp.get_diff_state_apply[state]().check_return_status() + if config_verify: + ccc_pnp.verify_diff_state_apply[state](config).check_return_status() + + module.exit_json(**ccc_pnp.result) + + +if __name__ == '__main__': + main() From b39b020e98e22041697f8048228e4b988d9fb78c Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 8 Feb 2024 10:40:29 +0000 Subject: [PATCH 19/64] Adding Playbook and Code for Provision workflow manager --- playbooks/device_provision_workflow.yml | 38 + plugins/modules/discovery_workflow_manager.py | 1 + plugins/modules/provision_workflow_manager.py | 735 ++++++++++++++++++ 3 files changed, 774 insertions(+) create mode 100644 playbooks/device_provision_workflow.yml create mode 100644 plugins/modules/provision_workflow_manager.py diff --git a/playbooks/device_provision_workflow.yml b/playbooks/device_provision_workflow.yml new file mode 100644 index 0000000000..362556a09f --- /dev/null +++ b/playbooks/device_provision_workflow.yml @@ -0,0 +1,38 @@ +--- +- name: Provision and Re-provision wired and wireless devices + hosts: localhost + connection: local + gather_facts: no + + vars_files: + - "{{ CLUSTERFILE }}" + + vars: + dnac_login: &dnac_login + dnac_host: "{{ dnac_host }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_port: "{{ dnac_port }}" + dnac_version: "{{ dnac_version }}" + dnac_debug: "{{ dnac_debug }}" + + tasks: + - name: Provision a wired device to a site + cisco.dnac.workflow_manager: + <<: *dnac_login + dnac_log: True + state: merged + config_verify: True + config: + - site_name_hierarchy: Global/USA/San Francisco/BGL_18 + management_ip_address: 204.1.1.1 + + + - name: Unprovision a wired device from a site + cisco.dnac.workflow_manager: + <<: *dnac_login + dnac_log: True + state: deleted + config: + - management_ip_address: 204.1.1.1 diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index b480deac86..a60d3148fb 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -248,6 +248,7 @@ timeout: integer username_list: list cli_cred_len: integer + - name: Delete disovery by name cisco.dnac.discovery_workflow_manager: dnac_host: "{{dnac_host}}" diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py new file mode 100644 index 0000000000..6fed5a33e4 --- /dev/null +++ b/plugins/modules/provision_workflow_manager.py @@ -0,0 +1,735 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ("Abinash Mishra, Madhan Sankaranarayanan") + +DOCUMENTATION = r""" +--- +module: provision_workflow_manager +short_description: Resource module for provision related functions +description: +- Manage operations related to wired and wireless provisioning +- API to re-provision provisioned devices +- API to un-provision provisioned devices +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.workflow_manager_params +author: Abinash Mishra (@abimishr) + Madhan Sankaranarayanan (@madsanka) +options: + config_verify: + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. + type: bool + default: False + state: + description: The state of Cisco Catalyst Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of device being managed. + type: list + elements: dict + required: true + suboptions: + management_ip_address: + description: Management Ip Address . + type: str + required: true + site_name_hierarchy: + description: Name of site where the device needs to be added. + type: str + managed_ap_locations: + description: Location of the sites allocated for the APs + type: list + elements: str + dynamic_interfaces: + description: Interface details of the controller + type: list + elements: dict + suboptions: + interface_ip_address: + description: Ip Address allocated to the interface + type: str + interface_netmask_in_c_i_d_r: + description: Ip Address allocated to the interface + type: int + interface_gateway: + description: Ip Address allocated to the interface + type: str + lag_or_port_number: + description: Ip Address allocated to the interface + type: int + vlan_id: + description: Ip Address allocated to the interface + type: int + interface_name: + description: Ip Address allocated to the interface + type: str + +requirements: +- dnacentersdk == 2.4.5 +- python >= 3.5 +notes: + - SDK Methods used are + sites.Sites.get_site, + devices.Devices.get_network_device_by_ip, + task.Task.get_task_by_id, + sda.Sda.get_provisioned_wired_device, + sda.Sda.re_provision_wired_device, + sda.Sda.provision_wired_device, + wireless.Wireless.provision + + - Paths used are + get /dna/intent/api/v1/site + get /dna/intent/api/v1/network-device/ip-address/{ipAddress} + get /dna/intent/api/v1/task/{taskId} + get /dna/intent/api/v1/business/sda/provision-device + put /dna/intent/api/v1/business/sda/provision-device + post /dna/intent/api/v1/business/sda/provision-device + post /dna/intent/api/v1/wireless/provision + +""" + +EXAMPLES = r""" +- name: Create/Modify a new provision + cisco.dnac.provision_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + state: merged + config: + - site_name_hierarchy: string + management_ip_address: string + managed_ap_locations: list + dynamic_interfaces: + - vlan_id: integer + interface_name: string + interface_ip_address: string + interface_gateway: string + interface_netmask_in_c_i_d_r: integer + lag_or_port_number: integer + +""" + +RETURN = r""" +# Case_1: Successful creation/updation/deletion of provision +response_1: + description: A dictionary with details of provision is returned + returned: always + type: dict + sample: > + { + "response": + { + "response": String, + "version": String + }, + "msg": String + } + +# Case_2: Error while creating a provision +response_2: + description: A list with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: list + sample: > + { + "response": [], + "msg": String + } + +# Case_3: Already exists and requires no update +response_3: + description: A dictionary with the exisiting details as returned by the Cisco Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "response": String, + "msg": String + } +""" +import time +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts +) + + +class Provision(DnacBase): + + """ + Class containing member attributes for provision workflow module + """ + def __init__(self, module): + super().__init__(module) + + def validate_input(self, state=None): + + """ + Validate the fields provided in the playbook. + Checks the configuration provided in the playbook against a predefined specification + to ensure it adheres to the expected structure and data types. + Args: + self: The instance of the class containing the 'config' attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of the + 'config' parameter. + Example: + To use this method, create an instance of the class and call 'validate_input' on it. + If the validation succeeds, 'self.status' will be 'success' and + 'self.validated_config' will contain the validated configuration. If it fails, + 'self.status' will be 'failed', and 'self.msg' will describe the validation issues. + """ + + if not self.config: + self.msg = "config not available in playbook for validattion" + self.status = "success" + return self + + provision_spec = { + "management_ip_address": {'type': 'str', 'required': True}, + "site_name_hierarchy": {'type': 'str', 'required': False}, + "managed_ap_locations": {'type': 'list', 'required': False, + 'elements': 'str'}, + "dynamic_interfaces": {'type': 'list', 'required': False, + 'elements': 'dict'} + } + if state == "merged": + provision_spec["site_name_hierarchy"] = {'type': 'str', 'required': True} + + # Validate provision params + valid_provision, invalid_params = validate_list_of_dicts( + self.config, provision_spec + ) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params)) + self.log(str(self.msg), "ERROR") + self.status = "failed" + return self + + self.validated_config = valid_provision + self.msg = "Successfully validated playbook configuration parameters using 'validate_input': {0}".format(str(valid_provision)) + self.log(str(self.msg), "INFO") + self.status = "success" + return self + + def get_dev_type(self): + """ + Fetches the type of device (wired/wireless) + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - device_type: A string indicating the type of the + device (wired/wireless). + Example: + Post creation of the validated input, we this method gets the + type of the device. + """ + + dev_response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"ip_address": self.validated_config[0]["management_ip_address"]} + ) + + self.log("The device response from 'get_network_device_by_ip' API is {0}".format(str(dev_response)), "DEBUG") + dev_dict = dev_response.get("response") + device_family = dev_dict["family"] + + if device_family == "Wireless Controller": + device_type = "wireless" + elif device_family in ["Switches and Hubs", "Routers"]: + device_type = "wired" + else: + device_type = None + self.log("The device type is {0}".format(device_type), "INFO") + return device_type + + def get_task_status(self, task_id=None): + """ + Fetches the status of the task once any provision API is called + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - result: A dict indiacting wheter the task was succesful or not + Example: + Post creation of the provision task, this method fetheches the task + status. + + """ + result = False + params = {"task_id": task_id} + while True: + response = self.dnac_apply['exec']( + family="task", + function='get_task_by_id', + params=params, + ) + self.log("Response collected from 'get_task_by_id' API is {0}".format(str(response)), "DEBUG") + response = response.response + self.log("Task status for the task id {0} is {1}".format(str(task_id), str(response)), "INFO") + if response.get('isError') or re.search( + 'failed', response.get('progress'), flags=re.IGNORECASE + ): + msg = 'Provision task with id {0} has not completed - Reason: {1}'.format( + task_id, response.get("failureReason")) + self.module.fail_json(msg=msg) + return False + + if response.get('progress') != 'In Progress': + result = True + break + + time.sleep(3) + self.result.update(dict(provision_task=response)) + return result + + def get_site_type(self, site_name_hierarchy=None): + """ + Fetches the type of site + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - site_type: A string indicating the type of the + site (area/building/floor). + Example: + Post creation of the validated input, we this method gets the + type of the site. + """ + + try: + response = self.dnac_apply['exec']( + family="sites", + function='get_site', + params={"name": site_name_hierarchy}, + ) + except Exception: + self.log("Exception occurred as \ + site '{0}' was not found".format(self.want.get("site_name")), "CRITICAL") + self.module.fail_json(msg="Site not found", response=[]) + + if response: + self.log("Received site details\ + for '{0}': {1}".format(site_name_hierarchy, str(response)), "DEBUG") + site = response.get("response") + site_additional_info = site[0].get("additionalInfo") + for item in site_additional_info: + if item["nameSpace"] == "Location": + site_type = item.get("attributes").get("type") + self.log("Site type for site name '{1}' : {0}".format(site_type, site_name_hierarchy), "INFO") + + return site_type + + def get_wired_params(self): + """ + Prepares the payload for provisioning of the wired devices + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - wired_params: A dictionary containing all the values indicating + management IP address of the device and the hierarchy + of the site. + Example: + Post creation of the validated input, it fetches the required + paramters and stores it for further processing and calling the + parameters in other APIs. + """ + + wired_params = { + "deviceManagementIpAddress": self.validated_config[0]["management_ip_address"], + "siteNameHierarchy": self.validated_config[0].get("site_name_hierarchy") + } + + self.log("Parameters collected for the provisioning of wired device:{0}".format(wired_params), "INFO") + return wired_params + + def get_wireless_params(self): + """ + Prepares the payload for provisioning of the wireless devices + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - wireless_params: A list of dictionary containing all the values indicating + management IP address of the device, hierarchy + of the site, AP Location of the wireless controller and details + of the interface + Example: + Post creation of the validated input, it fetches the required + paramters and stores it for further processing and calling the + parameters in other APIs. + """ + + wireless_params = [ + { + "site": self.validated_config[0].get("site_name_hierarchy"), + "managedAPLocations": self.validated_config[0].get("managed_ap_locations"), + } + ] + for ap_loc in wireless_params[0]["managedAPLocations"]: + if self.get_site_type(site_name_hierarchy=ap_loc) != "floor": + self.log("Managed AP Location must be a floor", "CRITICAL") + self.module.fail_json(msg="Managed AP Location must be a floor", response=[]) + + wireless_params[0]["dynamicInterfaces"] = [] + for interface in self.validated_config[0].get("dynamic_interfaces"): + interface_dict = { + "interfaceIPAddress": interface.get("interface_ip_address"), + "interfaceNetmaskInCIDR": interface.get("interface_netmask_in_c_i_d_r"), + "interfaceGateway": interface.get("interface_gateway"), + "lagOrPortNumber": interface.get("lag_or_port_number"), + "vlanId": interface.get("vlan_id"), + "interfaceName": interface.get("interface_name") + } + wireless_params[0]["dynamicInterfaces"].append(interface_dict) + response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"management_ip_address": self.validated_config[0]["management_ip_address"]} + ) + + self.log("Response collected from 'get_network_device_by_ip' is:{0}".format(str(response)), "DEBUG") + wireless_params[0]["deviceName"] = response.get("response")[0].get("hostname") + self.log("Parameters collected for the provisioning of wireless device:{0}".format(wireless_params), "INFO") + return wireless_params + + def get_want(self): + """ + Get all provision related informantion from the playbook + Args: + self: The instance of the class containing the 'config' attribute to be validated. + config: validated config passed from the playbook + Returns: + The method returns an instance of the class with updated attributes: + - self.want: A dictionary of paramters obtained from the playbook + - self.msg: A message indicating all the paramters from the playbook are + collected + - self.status: Success + Example: + It stores all the paramters passed from the playbook for further processing + before calling the APIs + """ + + self.want = {} + self.want["device_type"] = self.get_dev_type() + if self.want["device_type"] == "wired": + self.want["prov_params"] = self.get_wired_params() + elif self.want["device_type"] == "wireless": + self.want["prov_params"] = self.get_wireless_params() + else: + self.log("Passed devices are neither wired or wireless devices", "WARNING") + + self.msg = "Successfully collected all parameters from playbook " + \ + "for comparison" + self.log(self.msg, "INFO") + self.status = "success" + return self + + def get_diff_merged(self): + """ + Add to provision database + Args: + self: An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + object: An instance of the class with updated results and status + based on the processing of differences. + Description: + The function processes the differences and, depending on the + changes required, it may add, update,or resynchronize devices in + Cisco Catalyst Center. The updated results and status are stored in the + class instance for further use. + """ + + device_type = self.want.get("device_type") + if device_type == "wired": + try: + status_response = self.dnac_apply['exec']( + family="sda", + function="get_provisioned_wired_device", + op_modifies=True, + params={ + "device_management_ip_address": self.validated_config[0]["management_ip_address"] + }, + ) + except Exception: + status_response = {} + self.log("Wired device's status Response collected from 'get_provisioned_wired_device' API is:{0}".format(str(status_response)), "DEBUG") + status = status_response.get("status") + self.log("The provisioned status of the wired device is {0}".format(status), "INFO") + + if status == "success": + response = self.dnac_apply['exec']( + family="sda", + function="re_provision_wired_device", + op_modifies=True, + params=self.want["prov_params"], + ) + self.log("Reprovisioning response collected from 're_provision_wired_device' API is: {0}".format(response), "DEBUG") + else: + response = self.dnac_apply['exec']( + family="sda", + function="provision_wired_device", + op_modifies=True, + params=self.want["prov_params"], + ) + self.log("Provisioning response collected from 'provision_wired_device' API is: {0}".format(response), "DEBUG") + + elif device_type == "wireless": + response = self.dnac_apply['exec']( + family="wireless", + function="provision", + op_modifies=True, + params=self.want["prov_params"], + ) + self.log("Wireless provisioning response collected from 'provision' API is: {0}".format(response), "DEBUG") + + else: + self.result['msg'] = "Passed device is neither wired nor wireless" + self.log(self.result['msg'], "ERROR") + self.result['response'] = self.want["prov_params"] + return self + + task_id = response.get("taskId") + provision_info = self.get_task_status(task_id=task_id) + self.result["changed"] = True + self.result['msg'] = "Provision done Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = task_id + self.log(self.result['msg'], "INFO") + return self + + def get_diff_deleted(self): + """ + Delete from provision database + Args: + self: An instance of a class used for interacting with Cisco Catalyst Center + Returns: + self: An instance of the class with updated results and status based on + the deletion operation. + Description: + This function is responsible for removing devices from the Cisco Catalyst Center PnP GUI and + raise Exception if any error occured. + """ + + device_type = self.want.get("device_type") + + if device_type != "wired": + self.result['msg'] = "APIs are not supported for the device" + self.log(self.result['msg'], "CRITICAL") + return self + + try: + status_response = self.dnac_apply['exec']( + family="sda", + function="get_provisioned_wired_device", + op_modifies=True, + params={ + "device_management_ip_address": self.validated_config[0]["management_ip_address"] + }, + ) + + except Exception: + status_response = {} + self.log("Wired device's status Response collected from 'get_provisioned_wired_device' API is:{0}".format(str(status_response)), "DEBUG") + status = status_response.get("status") + self.log("The provisioned status of the wired device is {0}".format(status), "INFO") + + if status != "success": + self.result['msg'] = "Device associated with the passed IP address is not provisioned" + self.log(self.result['msg'], "CRITICAL") + self.result['response'] = self.want["prov_params"] + return self + + response = self.dnac_apply['exec']( + family="sda", + function="delete_provisioned_wired_device", + op_modifies=True, + params={ + "device_management_ip_address": self.validated_config[0]["management_ip_address"] + }, + ) + self.log("Response collected from the 'delete_provisioned_wired_device' API is : {0}".format(str(response)), "DEBUG") + + task_id = response.get("taskId") + deletion_info = self.get_task_status(task_id=task_id) + self.result["changed"] = True + self.result['msg'] = "Deletion done Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = task_id + self.log(self.result['msg'], "INFO") + return self + + def verify_diff_merged(self): + """ + Verify the merged status(Creation/Updation) of Discovery in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the merged status of a configuration in Cisco Catalyst Center by + retrieving the current state (have) and desired state (want) of the configuration, + logs the states, and validates whether the specified device(s) exists in the DNA + Center configuration's Inventory Database in the provisioned state. + """ + + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + # Code to validate Cisco Catalyst Center config for merged state + + device_type = self.want.get("device_type") + if device_type == "wired": + try: + status_response = self.dnac_apply['exec']( + family="sda", + function="get_provisioned_wired_device", + op_modifies=True, + params={ + "device_management_ip_address": self.validated_config[0]["management_ip_address"] + }, + ) + except Exception: + status_response = {} + self.log("Wired device's status Response collected from 'get_provisioned_wired_device' API is:{0}".format(str(status_response)), "DEBUG") + status = status_response.get("status") + self.log("The provisioned status of the wired device is {0}".format(status), "INFO") + + if status == "success": + self.log("Requested wired device is alread provisioned", "INFO") + + else: + self.log("Requested wired device is not provisioned", "INFO") + + else: + self.log("Currently we don't have any API in the Cisco Catalyst Center to fetch the provisioning details of wired devices") + self.status = "success" + + return self + + def verify_diff_deleted(self): + """ + Verify the deletion status of Discovery in Cisco Catalyst Center. + Args: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified discovery(s) exists in the Cisco Catalyst Center configuration's + Inventory Database in the provisioned state. + """ + + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + # Code to validate Cisco Catalyst Center config for merged state + + device_type = self.want.get("device_type") + if device_type == "wired": + try: + status_response = self.dnac_apply['exec']( + family="sda", + function="get_provisioned_wired_device", + op_modifies=True, + params={ + "device_management_ip_address": self.validated_config[0]["management_ip_address"] + }, + ) + except Exception: + status_response = {} + self.log("Wired device's status Response collected from 'get_provisioned_wired_device' API is:{0}".format(str(status_response)), "DEBUG") + status = status_response.get("status") + self.log("The provisioned status of the wired device is {0}".format(status), "INFO") + + if status == "success": + self.log("Requested wired device is in provisioned state and is not unprovisioned", "INFO") + + else: + self.log("Requested wired device is unprovisioned", "INFO") + + else: + self.log("Currently we don't have any API in the Cisco Catalyst Center to fetch the provisioning details of wired devices") + self.status = "success" + + return self + + +def main(): + + """ + main entry point for module execution + """ + + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + "dnac_log_level": {"type": 'str', "default": 'WARNING'}, + "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, + "dnac_log_append": {"type": 'bool', "default": True}, + "config_verify": {"type": 'bool', "default": False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + ccc_provision = Provision(module) + config_verify = ccc_provision.params.get("config_verify") + + state = ccc_provision.params.get("state") + if state not in ccc_provision.supported_states: + ccc_provision.status = "invalid" + ccc_provision.msg = "State {0} is invalid".format(state) + ccc_provision.check_return_status() + + ccc_provision.validate_input(state=state).check_return_status() + + for config in ccc_provision.validated_config: + ccc_provision.reset_values() + ccc_provision.get_want().check_return_status() + ccc_provision.get_diff_state_apply[state]().check_return_status() + if config_verify: + ccc_provision.verify_diff_state_apply[state]().check_return_status() + + module.exit_json(**ccc_provision.result) + + +if __name__ == '__main__': + main() From a8f69aafb9a7069443cd445a78c8689a6e8ded4e Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 8 Feb 2024 10:55:52 +0000 Subject: [PATCH 20/64] Fixing naming conventions --- plugins/modules/discovery_workflow_manager.py | 2 +- plugins/modules/pnp_workflow_manager.py | 2 +- plugins/modules/provision_workflow_manager.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index a60d3148fb..f4641bc074 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -21,7 +21,7 @@ - cisco.dnac.workflow_manager_params author: Abinash Mishra (@abimishr) Phan Nguyen (@phannguy) - Madhan Sankaranarayanan (@madsanka) + Madhan Sankaranarayanan (@madhansansel) options: config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 4e662433e5..fb9e35f918 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -6,7 +6,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -__author__ = ("Abinash Mishra, MadhanSankaranarayanan, Rishita Chowdhary") +__author__ = ("Abinash Mishra, Madhan Sankaranarayanan, Rishita Chowdhary") DOCUMENTATION = r""" --- diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index 6fed5a33e4..5c723a14d0 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -20,7 +20,7 @@ extends_documentation_fragment: - cisco.dnac.workflow_manager_params author: Abinash Mishra (@abimishr) - Madhan Sankaranarayanan (@madsanka) + Madhan Sankaranarayanan (@madhansansel) options: config_verify: description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. From bcee95faaef636e2ec8b56c8751e48324745d7e2 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 12 Feb 2024 12:49:06 +0530 Subject: [PATCH 21/64] Remove snmp mode default value of AUTHPRIV while updating device and add check in the code to set snmpMode while updating device if not given in playbook --- plugins/modules/inventory_intent.py | 17 +++++++++++++++-- plugins/modules/inventory_workflow_manager.py | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 045a99e3f9..dc2f27261f 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -106,7 +106,6 @@ snmp_mode: description: Device's snmp Mode. type: str - default: "AUTHPRIV" snmp_priv_passphrase: description: Device's snmp Private Passphrase. Required for Adding Network, Compute, Third Party Devices. type: str @@ -754,7 +753,7 @@ def validate_input(self): 'serial_number': {'type': 'str'}, 'snmp_auth_passphrase': {'type': 'str'}, 'snmp_auth_protocol': {'default': "SHA", 'type': 'str'}, - 'snmp_mode': {'default': "AUTHPRIV", 'type': 'str'}, + 'snmp_mode': {'type': 'str'}, 'snmp_priv_passphrase': {'type': 'str'}, 'snmp_priv_protocol': {'type': 'str'}, 'snmp_ro_community': {'default': "public", 'type': 'str'}, @@ -2708,6 +2707,18 @@ def get_diff_merged(self, config): playbook_params = self.want.get("device_params").copy() playbook_params['ipAddress'] = [device_ip] device_data = device_details[device_ip] + if device_data['snmpv3_privacy_password'] == ' ': + device_data['snmpv3_privacy_password'] = None + if device_data['snmpv3_auth_password'] == ' ': + device_data['snmpv3_auth_password'] = None + + if not playbook_params['snmpMode']: + if device_data['snmpv3_privacy_password']: + playbook_params['snmpMode'] = "AUTHPRIV" + elif device_data['snmpv3_auth_password']: + playbook_params['snmpMode'] = "AUTHNOPRIV" + else: + playbook_params['snmpMode'] = "NOAUTHNOPRIV" if not playbook_params['cliTransport']: if device_data['protocol'] == "ssh2": @@ -2877,6 +2888,8 @@ def get_diff_merged(self, config): if device_added: config['ip_address'] = devices_to_add device_params = self.want.get("device_params") + if not device_params['snmpMode']: + device_params['snmpMode'] = "AUTHPRIV" if not device_params['cliTransport']: device_params['cliTransport'] = "ssh" diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 2330395929..1b42f9f4e1 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -106,7 +106,6 @@ snmp_mode: description: Device's snmp Mode. type: str - default: "AUTHPRIV" snmp_priv_passphrase: description: Device's snmp Private Passphrase. Required for Adding Network, Compute, Third Party Devices. type: str @@ -754,7 +753,7 @@ def validate_input(self): 'serial_number': {'type': 'str'}, 'snmp_auth_passphrase': {'type': 'str'}, 'snmp_auth_protocol': {'default': "SHA", 'type': 'str'}, - 'snmp_mode': {'default': "AUTHPRIV", 'type': 'str'}, + 'snmp_mode': {'type': 'str'}, 'snmp_priv_passphrase': {'type': 'str'}, 'snmp_priv_protocol': {'type': 'str'}, 'snmp_ro_community': {'default': "public", 'type': 'str'}, @@ -2708,6 +2707,18 @@ def get_diff_merged(self, config): playbook_params = self.want.get("device_params").copy() playbook_params['ipAddress'] = [device_ip] device_data = device_details[device_ip] + if device_data['snmpv3_privacy_password'] == ' ': + device_data['snmpv3_privacy_password'] = None + if device_data['snmpv3_auth_password'] == ' ': + device_data['snmpv3_auth_password'] = None + + if not playbook_params['snmpMode']: + if device_data['snmpv3_privacy_password']: + playbook_params['snmpMode'] = "AUTHPRIV" + elif device_data['snmpv3_auth_password']: + playbook_params['snmpMode'] = "AUTHNOPRIV" + else: + playbook_params['snmpMode'] = "NOAUTHNOPRIV" if not playbook_params['cliTransport']: if device_data['protocol'] == "ssh2": @@ -2877,6 +2888,8 @@ def get_diff_merged(self, config): if device_added: config['ip_address'] = devices_to_add device_params = self.want.get("device_params") + if not device_params['snmpMode']: + device_params['snmpMode'] = "AUTHPRIV" if not device_params['cliTransport']: device_params['cliTransport'] = "ssh" From b78480d3147275fd050b94711e65f5b5f08f0a64 Mon Sep 17 00:00:00 2001 From: Abinash Date: Mon, 12 Feb 2024 09:00:46 +0000 Subject: [PATCH 22/64] Adding small changes post testing in Discovery --- plugins/modules/discovery_workflow_manager.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index f4641bc074..4e088e15da 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -879,6 +879,12 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): self.log("Retrieved device details using the API 'get_discovered_network_devices_by_discovery_id': {0}".format(str(devices)), "DEBUG") if all(res.get('reachabilityStatus') == 'Success' for res in devices): result = True + self.log("All devices in the range are reachable", "INFO") + break + + elif any(res.get('reachabilityStatus') == 'Success' for res in devices): + result = True + self.log("Some devices in the range are reachable", "INFO") break count += 1 From 6b5b7e737ef2f6b67580935aa6831cbe6bf892ec Mon Sep 17 00:00:00 2001 From: Abinash Date: Mon, 12 Feb 2024 09:35:23 +0000 Subject: [PATCH 23/64] Pushing the latest changes in Intent module --- plugins/modules/discovery_intent.py | 100 ++++++++++++++++------------ 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 5e8dcfbc82..2b44dae2b0 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -1,12 +1,12 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright (c) 2022, Cisco Systems +# Copyright (c) 2024, Cisco Systems # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type -__author__ = ("Abinash Mishra, Phan Nguyen") +__author__ = ("Abinash Mishra, Phan Nguyen, Madhan Sankaranarayanan") DOCUMENTATION = r""" --- @@ -20,14 +20,15 @@ extends_documentation_fragment: - cisco.dnac.intent_params author: Abinash Mishra (@abimishr) - Phan Nguyen (phannguy) + Phan Nguyen (@phannguy) + Madhan Sankaranarayanan (@madhansansel) options: config_verify: - description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool default: False state: - description: The state of DNAC after module completion. + description: The state of Cisco Catalyst Center after module completion. type: str choices: [ merged, deleted ] default: merged @@ -39,7 +40,11 @@ required: true suboptions: ip_address_list: - description: List of IP addresses to be discoverred. + description: List of IP addresses to be discovered. For CDP/LLDP/SINGLE based discovery, we should + pass a list with single element like - 10.197.156.22. For CIDR based discovery, we should pass a list with + single element like - 10.197.156.22/22. For RANGE based discovery, we should pass a list with single element + and range like - 10.197.156.1-10.197.156.100. For MULTI RANGE based discovery, we should pass a list with multiple + elementd like - 10.197.156.1-10.197.156.100 and in next line - 10.197.157.1-10.197.157.100. type: list elements: str required: true @@ -243,6 +248,7 @@ timeout: integer username_list: list cli_cred_len: integer + - name: Delete disovery by name cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" @@ -263,7 +269,7 @@ RETURN = r""" #Case_1: When the device(s) are discovered successfully. response_1: - description: A dictionary with the response returned by the Cisco DNAC Python SDK + description: A dictionary with the response returned by the Cisco Catalyst Center Python SDK returned: always type: dict sample: > @@ -278,7 +284,7 @@ #Case_2: Given device details or SNMP mode are not provided response_2: - description: A list with the response returned by the Cisco DNAC Python SDK + description: A list with the response returned by the Cisco Catalyst Center Python SDK returned: always type: list sample: > @@ -289,7 +295,7 @@ #Case_3: Error while deleting a discovery response_3: - description: A string with the response returned by the Cisco DNAC Python SDK + description: A string with the response returned by the Cisco Catalyst Center Python SDK returned: always type: dict sample: > @@ -307,7 +313,7 @@ import re -class DnacDiscovery(DnacBase): +class Discovery(DnacBase): def __init__(self, module): """ Initialize an instance of the class. It also initializes an empty @@ -435,7 +441,7 @@ def get_creds_ids_list(self): self.log("Credential Ids list passed is {0}".format(str(self.creds_ids_list)), "INFO") return self.creds_ids_list - def get_dnac_global_credentials_v2_info(self): + def get_ccc_global_credentials_v2_info(self): """ Retrieve the global credentials information (version 2). It applies the 'get_all_global_credentials_v2' function and extracts @@ -679,7 +685,7 @@ def create_params(self, credential_ids=None, ip_address_list=None): def create_discovery(self, credential_ids=None, ip_address_list=None): """ - Start a new discovery process in the DNA Center. It creates the + Start a new discovery process in the Cisco Catalyst Center. It creates the parameters required for the discovery and then calls the 'start_discovery' function. The result of the discovery process is added to the 'result' attribute. @@ -713,7 +719,7 @@ def create_discovery(self, credential_ids=None, ip_address_list=None): def get_task_status(self, task_id=None): """ - Monitor the status of a task in the DNA Center. It checks the task + Monitor the status of a task in the Cisco Catalyst Center. It checks the task status periodically until the task is no longer 'In Progress'. If the task encounters an error or fails, it immediately fails the module and returns False. @@ -756,7 +762,7 @@ def get_task_status(self, task_id=None): def lookup_discovery_by_range_via_name(self): """ Retrieve a specific discovery by name from a range of - discoveries in the DNA Center. + discoveries in the Cisco Catalyst Center. Returns: - discovery: The discovery with the specified name from the range @@ -805,7 +811,7 @@ def lookup_discovery_by_range_via_name(self): def get_discoveries_by_range_until_success(self): """ Continuously retrieve a specific discovery by name from a range of - discoveries in the DNA Center until the discovery is complete. + discoveries in the Cisco Catalyst Center until the discovery is complete. Returns: - discovery: The completed discovery with the specified name from @@ -843,7 +849,7 @@ def get_discoveries_by_range_until_success(self): def get_discovery_device_info(self, discovery_id=None, task_id=None): """ Retrieve the information of devices discovered by a specific discovery - process in the DNA Center. It checks the reachability status of the + process in the Cisco Catalyst Center. It checks the reachability status of the devices periodically until all devices are reachable or until a maximum of 3 attempts. @@ -873,6 +879,12 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): self.log("Retrieved device details using the API 'get_discovered_network_devices_by_discovery_id': {0}".format(str(devices)), "DEBUG") if all(res.get('reachabilityStatus') == 'Success' for res in devices): result = True + self.log("All devices in the range are reachable", "INFO") + break + + elif any(res.get('reachabilityStatus') == 'Success' for res in devices): + result = True + self.log("Some devices in the range are reachable", "INFO") break count += 1 @@ -912,7 +924,7 @@ def get_exist_discovery(self): def delete_exist_discovery(self, params): """ - Delete an existing discovery in the DNA Center by its ID. + Delete an existing discovery in the Cisco Catalyst Center by its ID. Parameters: - params: A dictionary containing the parameters for the delete @@ -936,7 +948,7 @@ def delete_exist_discovery(self, params): def get_diff_merged(self): """ Retrieve the information of devices discovered by a specific discovery - process in the DNA Center, delete existing discoveries if they exist, + process in the Cisco Catalyst Center, delete existing discoveries if they exist, and create a new discovery. The function also updates various attributes of the class instance. @@ -944,7 +956,7 @@ def get_diff_merged(self): - self: The instance of the class with updated attributes. """ - self.get_dnac_global_credentials_v2_info() + self.get_ccc_global_credentials_v2_info() devices_list_info = self.get_devices_list_info() ip_address_list = self.preprocess_device_discovery(devices_list_info) exist_discovery = self.get_exist_discovery() @@ -969,7 +981,7 @@ def get_diff_merged(self): def get_diff_deleted(self): """ - Delete an existing discovery in the DNA Center by its name, and + Delete an existing discovery in the Cisco Catalyst Center by its name, and updates various attributes of the class instance. If no discovery with the specified name is found, the function updates the 'msg' attribute with an appropriate message. @@ -1020,14 +1032,14 @@ def get_diff_deleted(self): def verify_diff_merged(self, config): """ - Verify the merged status(Creation/Updation) of Discovery in Cisco DNA Center. + Verify the merged status(Creation/Updation) of Discovery in Cisco Catalyst Center. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): The configuration details to be verified. Return: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method checks the merged status of a configuration in Cisco DNA Center by + This method checks the merged status of a configuration in Cisco Catalyst Center by retrieving the current state (have) and desired state (want) of the configuration, logs the states, and validates whether the specified device(s) exists in the DNA Center configuration's Discovery Database. @@ -1035,7 +1047,7 @@ def verify_diff_merged(self, config): self.log("Current State (have): {0}".format(str(self.have)), "INFO") self.log("Desired State (want): {0}".format(str(config)), "INFO") - # Code to validate dnac config for merged state + # Code to validate Cisco Catalyst Center config for merged state discovery_task_info = self.get_discoveries_by_range_until_success() discovery_id = discovery_task_info.get('id') params = dict( @@ -1058,21 +1070,21 @@ def verify_diff_merged(self, config): def verify_diff_deleted(self, config): """ - Verify the deletion status of Discovery in Cisco DNA Center. + Verify the deletion status of Discovery in Cisco Catalyst Center. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): The configuration details to be verified. Return: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method checks the deletion status of a configuration in Cisco DNA Center. - It validates whether the specified discovery(s) exists in the DNA Center configuration's + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified discovery(s) exists in the Cisco Catalyst Center configuration's Discovery Database. """ self.log("Current State (have): {0}".format(str(self.have)), "INFO") self.log("Desired State (want): {0}".format(str(config)), "INFO") - # Code to validate dnac config for deleted state + # Code to validate Cisco Catalyst Center config for deleted state discovery_task_info = self.lookup_discovery_by_range_via_name() discovery_name = config.get('discovery_name') if discovery_task_info: @@ -1109,23 +1121,23 @@ def main(): module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) - dnac_discovery = DnacDiscovery(module) - config_verify = dnac_discovery.params.get("config_verify") + ccc_discovery = Discovery(module) + config_verify = ccc_discovery.params.get("config_verify") - state = dnac_discovery.params.get("state") - if state not in dnac_discovery.supported_states: - dnac_discovery.status = "invalid" - dnac_discovery.msg = "State {0} is invalid".format(state) - dnac_discovery.check_return_status() + state = ccc_discovery.params.get("state") + if state not in ccc_discovery.supported_states: + ccc_discovery.status = "invalid" + ccc_discovery.msg = "State {0} is invalid".format(state) + ccc_discovery.check_return_status() - dnac_discovery.validate_input(state=state).check_return_status() - for config in dnac_discovery.validated_config: - dnac_discovery.reset_values() - dnac_discovery.get_diff_state_apply[state]().check_return_status() + ccc_discovery.validate_input(state=state).check_return_status() + for config in ccc_discovery.validated_config: + ccc_discovery.reset_values() + ccc_discovery.get_diff_state_apply[state]().check_return_status() if config_verify: - dnac_discovery.verify_diff_state_apply[state](config).check_return_status() + ccc_discovery.verify_diff_state_apply[state](config).check_return_status() - module.exit_json(**dnac_discovery.result) + module.exit_json(**ccc_discovery.result) if __name__ == '__main__': From 8d9c2d6da16dce13fa55d36af89bc8f8b961ed68 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 13 Feb 2024 20:43:57 +0530 Subject: [PATCH 24/64] Replace device_type in swim module for tagging/un-tagging image with device_image_family_name, add brief description for enum or choices for parameter, changed boolean value from true/false to True/False in all modules --- playbooks/swim_workflow_manager.yml | 11 +- plugins/modules/inventory_intent.py | 45 +++--- plugins/modules/inventory_workflow_manager.py | 45 +++--- plugins/modules/swim_intent.py | 133 ++++++++++++------ plugins/modules/swim_workflow_manager.py | 123 ++++++++++------ 5 files changed, 233 insertions(+), 124 deletions(-) diff --git a/playbooks/swim_workflow_manager.yml b/playbooks/swim_workflow_manager.yml index 237fcaec4b..29bc027374 100644 --- a/playbooks/swim_workflow_manager.yml +++ b/playbooks/swim_workflow_manager.yml @@ -16,9 +16,9 @@ dnac_port: "{{dnac_port}}" dnac_version: "{{dnac_version}}" dnac_debug: "{{dnac_debug}}" - dnac_log: true + dnac_log: True dnac_log_level: DEBUG - config_verify: true + config_verify: True config: - import_image_details: type: "{{ item.type }}" @@ -30,8 +30,7 @@ image_name: "{{item.image_name}}" site_name: "{{item.site_name}}" device_role: "{{ item.device_role }}" - device_family_name: "{{ item.device_family_name }}" - device_type: "{{item.device_type}}" + device_image_family_name: "{{ item.device_image_family_name }}" tagging: false image_distribution_details: image_name: "{{item.image_name}}" @@ -42,8 +41,8 @@ site_name: "{{item.site_name}}" device_role: "{{ item.device_role }}" device_family_name: "{{ item.device_family_name }}" - scehdule_validate: false - distribute_if_needed: true + scehdule_validate: False + distribute_if_needed: True with_items: "{{ image_details }}" tags: diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index dc2f27261f..85ad9b6d3f 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -181,6 +181,19 @@ default: false role: description: Role of device which can be ACCESS, CORE, DISTRIBUTION, BORDER ROUTER, UNKNOWN. + ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. + UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. + This could happen if the platform is unable to determine the device's role based on available information. + ACCESS - This role typically represents switches or access points that serve as access points for end-user devices to connect to the network. + These devices are often located at the edge of the network and provide connectivity to end-user devices. + BORDER ROUTER - These are devices that connect different network domains or segments together. They often serve as + gateways between different networks, such as connecting an enterprise network to the internet or connecting + multiple branch offices. + DISTRIBUTION - This role represents function as distribution switches or routers in hierarchical network designs. They aggregate traffic + from access switches and route it toward the core of the network or toward other distribution switches. + CORE - This role typically represents high-capacity switches or routers that form the backbone of the network. They handle large volumes + of traffic and provide connectivity between different parts of network, such as connecting distribution switches or + providing interconnection between different network segments. type: str default: "ACCESS" role_source: @@ -315,7 +328,7 @@ snmp_username: string snmp_version: string type: string - device_added: true + device_added: True username: string - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device @@ -344,9 +357,9 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: string - compute_device: true + compute_device: True username: string - device_added: true + device_added: True type: "COMPUTE_DEVICE" - name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. @@ -363,7 +376,7 @@ state: merged config: - http_password: string - device_added: true + device_added: True type: "MERAKI_DASHBOARD" - name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. @@ -384,7 +397,7 @@ http_username: string http_password: string http_port: string - device_added: true + device_added: True type: "FIREPOWER_MANAGEMENT_SYSTEM" - name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. @@ -410,7 +423,7 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: string - device_added: true + device_added: True type: "THIRD_PARTY_DEVICE" - name: Update device details or credentails in Inventory @@ -447,8 +460,8 @@ snmp_username: string snmp_version: string type: string - device_updated: true - credential_update: true + device_updated: True + credential_update: True update_mgmt_ipaddresslist: - exist_mgmt_ipaddress: string new_mgmt_ipaddress: string @@ -467,10 +480,10 @@ dnac_log: False state: merged config: - - device_updated: true + - device_updated: True ip_address: - string - credential_update: true + credential_update: True update_mgmt_ipaddresslist: - exist_mgmt_ipaddress: string new_mgmt_ipaddress: string @@ -535,7 +548,7 @@ config: - ip_address: - string - device_updated: true + device_updated: True update_device_role: role: string role_source: string @@ -555,7 +568,7 @@ config: - ip_address: - string - device_updated: true + device_updated: True update_interface_details: description: str admin_status: str @@ -619,7 +632,7 @@ config: - ip_address: - string - device_resync: true + device_resync: True force_sync: false - name: Reboot AP Devices with IP Addresses @@ -637,7 +650,7 @@ config: - ip_address: - string - reboot_device: true + reboot_device: True - name: Delete Provision/Unprovision Devices by IP Address cisco.dnac.inventory_intent: @@ -1755,9 +1768,9 @@ def get_site_type(self, site_name): site_type = item.get("attributes").get("type") except Exception as e: - self.msg = "Error while fetching the site '{0}'.".format(site_name) + self.msg = "Error while fetching the site '{0}' and given site not found in Cisco Catalyst Center".format(site_name) self.log(self.msg, "ERROR") - self.module.fail_json(msg="Site not found", response=[]) + self.module.fail_json(msg=self.msg, response=[self.msg]) return site_type diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 1b42f9f4e1..ac822b9cd8 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -181,6 +181,19 @@ default: false role: description: Role of device which can be ACCESS, CORE, DISTRIBUTION, BORDER ROUTER, UNKNOWN. + ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. + UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. + This could happen if the platform is unable to determine the device's role based on available information. + ACCESS - This role typically represents switches or access points that serve as access points for end-user devices to connect to the network. + These devices are often located at the edge of the network and provide connectivity to end-user devices. + BORDER ROUTER - These are devices that connect different network domains or segments together. They often serve as + gateways between different networks, such as connecting an enterprise network to the internet or connecting + multiple branch offices. + DISTRIBUTION - This role represents function as distribution switches or routers in hierarchical network designs. They aggregate traffic + from access switches and route it toward the core of the network or toward other distribution switches. + CORE - This role typically represents high-capacity switches or routers that form the backbone of the network. They handle large volumes + of traffic and provide connectivity between different parts of network, such as connecting distribution switches or + providing interconnection between different network segments. type: str default: "ACCESS" role_source: @@ -315,7 +328,7 @@ snmp_username: string snmp_version: string type: string - device_added: true + device_added: True username: string - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device @@ -344,9 +357,9 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: string - compute_device: true + compute_device: True username: string - device_added: true + device_added: True type: "COMPUTE_DEVICE" - name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. @@ -363,7 +376,7 @@ state: merged config: - http_password: string - device_added: true + device_added: True type: "MERAKI_DASHBOARD" - name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. @@ -384,7 +397,7 @@ http_username: string http_password: string http_port: string - device_added: true + device_added: True type: "FIREPOWER_MANAGEMENT_SYSTEM" - name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. @@ -410,7 +423,7 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: string - device_added: true + device_added: True type: "THIRD_PARTY_DEVICE" - name: Update device details or credentails in Inventory @@ -447,8 +460,8 @@ snmp_username: string snmp_version: string type: string - device_updated: true - credential_update: true + device_updated: True + credential_update: True update_mgmt_ipaddresslist: - exist_mgmt_ipaddress: string new_mgmt_ipaddress: string @@ -467,10 +480,10 @@ dnac_log: False state: merged config: - - device_updated: true + - device_updated: True ip_address: - string - credential_update: true + credential_update: True update_mgmt_ipaddresslist: - exist_mgmt_ipaddress: string new_mgmt_ipaddress: string @@ -535,7 +548,7 @@ config: - ip_address: - string - device_updated: true + device_updated: True update_device_role: role: string role_source: string @@ -555,7 +568,7 @@ config: - ip_address: - string - device_updated: true + device_updated: True update_interface_details: description: str admin_status: str @@ -619,7 +632,7 @@ config: - ip_address: - string - device_resync: true + device_resync: True force_sync: false - name: Reboot AP Devices with IP Addresses @@ -637,7 +650,7 @@ config: - ip_address: - string - reboot_device: true + reboot_device: True - name: Delete Provision/Unprovision Devices by IP Address cisco.dnac.inventory_workflow_manager: @@ -1755,9 +1768,9 @@ def get_site_type(self, site_name): site_type = item.get("attributes").get("type") except Exception as e: - self.msg = "Error while fetching the site '{0}'.".format(site_name) + self.msg = "Error while fetching the site '{0}' and given site not found in Cisco Catalyst Center".format(site_name) self.log(self.msg, "ERROR") - self.module.fail_json(msg="Site not found", response=[]) + self.module.fail_json(msg=self.msg, response=[self.msg]) return site_type diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 1e79f45ca2..80f279fcd7 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -50,26 +50,26 @@ type: dict suboptions: type: - description: The source of import, supports url import or local import. + description: The source of import, supports remote import or local import. type: str local_image_details: description: Details of the local path of the image to be imported. type: dict suboptions: file_path: - description: File absolute path. + description: Give the file absolute path required while importing image from local. type: str is_third_party: - description: IsThirdParty query parameter. Third party Image check. + description: IsThirdParty query parameter. Third party Image check (Optional). type: bool third_party_application_type: - description: ThirdPartyApplicationType query parameter. Third Party Application Type. + description: ThirdPartyApplicationType query parameter. Third Party Application Type (Optional). type: str third_party_image_family: - description: ThirdPartyImageFamily query parameter. Third Party image family. + description: ThirdPartyImageFamily query parameter. Third Party image family (Optional). type: str third_party_vendor: - description: ThirdPartyVendor query parameter. Third Party Vendor. + description: ThirdPartyVendor query parameter (Optional). type: str url_details: description: URL details for SWIM import @@ -81,19 +81,20 @@ elements: dict suboptions: application_type: - description: Swim Import Via Url's applicationType. + description: Optional parameter indicating the type of application with permitted values(WLC, LINUX, FILREWALL, WINDOWS, + LOADBALANCER, THIRDPARTY etc) applicable only for third party image types. type: str image_family: - description: Swim Import Via Url's imageFamily. + description: The name of image family applicable only in case of third party images upload (Optional). type: str source_url: - description: Swim Import Image Via Url. + description: Required parameter for importing swim image via remote url. type: str is_third_party: - description: ThirdParty flag. + description: Set the boolean value if image is uploaded from third party (Optional). type: bool vendor: - description: Swim Import Via Url's vendor. + description: Name of vendor applicable only for third party image types (Optional). type: str schedule_at: description: ScheduleAt query parameter. Epoch Time (The number of milli-seconds since @@ -115,12 +116,22 @@ device_role: description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, DISTRIBUTION and CORE. + ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. + UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. + This could happen if the platform is unable to determine the device's role based on available information. + ACCESS - This role typically represents switches or access points that serve as access points for end-user devices to connect to the network. + These devices are often located at the edge of the network and provide connectivity to end-user devices. + BORDER ROUTER - These are devices that connect different network domains or segments together. They often serve as + gateways between different networks, such as connecting an enterprise network to the internet or connecting + multiple branch offices. + DISTRIBUTION - This role represents function as distribution switches or routers in hierarchical network designs. They aggregate traffic + from access switches and route it toward the core of the network or toward other distribution switches. + CORE - This role typically represents high-capacity switches or routers that form the backbone of the network. They handle large volumes + of traffic and provide connectivity between different parts of network, such as connecting distribution switches or + providing interconnection between different network segments. type: str - device_family_name: - description: Device family name(Eg Switches and Hubs) - type: str - device_type: - description: Type of the device (Eg Cisco Catalyst 9300 Switch) + device_image_family_name: + description: Device Image family name(Eg Cisco Catalyst 9300 Switch) type: str site_name: description: Site name for which SWIM image will be tagged/untagged as golden. @@ -140,9 +151,22 @@ device_role: description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, DISTRIBUTION and CORE. + ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. + UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. + This could happen if the platform is unable to determine the device's role based on available information. + ACCESS - This role typically represents switches or access points that serve as access points for end-user devices to connect to the network. + These devices are often located at the edge of the network and provide connectivity to end-user devices. + BORDER ROUTER - These are devices that connect different network domains or segments together. They often serve as + gateways between different networks, such as connecting an enterprise network to the internet or connecting + multiple branch offices. + DISTRIBUTION - This role represents function as distribution switches or routers in hierarchical network designs. They aggregate traffic + from access switches and route it toward the core of the network or toward other distribution switches. + CORE - This role typically represents high-capacity switches or routers that form the backbone of the network. They handle large volumes + of traffic and provide connectivity between different parts of network, such as connecting distribution switches or + providing interconnection between different network segments. type: str device_family_name: - description: Device family name + description: Name of the device family(Switches and Hubs etc.) type: str site_name: description: Used to get device details associated to this site. @@ -169,11 +193,24 @@ type: dict suboptions: device_role: - description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, + description: Device Role and permissible Values are ALL, UNKNOWN, ACCESS, BORDER ROUTER, DISTRIBUTION and CORE. + ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. + UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. + This could happen if the platform is unable to determine the device's role based on available information. + ACCESS - This role typically represents switches or access points that serve as access points for end-user devices to connect to the network. + These devices are often located at the edge of the network and provide connectivity to end-user devices. + BORDER ROUTER - These are devices that connect different network domains or segments together. They often serve as + gateways between different networks, such as connecting an enterprise network to the internet or connecting + multiple branch offices. + DISTRIBUTION - This role represents function as distribution switches or routers in hierarchical network designs. They aggregate traffic + from access switches and route it toward the core of the network or toward other distribution switches. + CORE - This role typically represents high-capacity switches or routers that form the backbone of the network. They handle large volumes + of traffic and provide connectivity between different parts of network, such as connecting distribution switches or + providing interconnection between different network segments. type: str device_family_name: - description: Device family name + description: Name of the device family(Switches and Hubs etc.) type: str site_name: description: Used to get device details associated to this site. @@ -182,10 +219,20 @@ description: ActivateLowerImageVersion flag. type: bool device_upgrade_mode: - description: Swim Trigger Activation's deviceUpgradeMode. + description: It specifies the mode of upgrade to be applied to the devices having the following values - 'install', 'bundle', and 'currentlyExists'. + install - This mode instructs Cisco Catalyst Center to perform a clean installation of the new image on the target devices. + When this mode is selected, the existing image on the device is completely replaced with the new image during the upgrade process. + This ensures that the device runs only the new image version after the upgrade is completed. + bundle - This mode instructs Cisco Catalyst Center bundles the new image with the existing image on the device before initiating + the upgrade process.This mode allows for a more efficient upgrade process by preserving the existing image on the device while + adding the new image as an additional bundle.After the upgrade, the device can run either the existing image or the new bundled + image, depending on the configuration. + currentlyExists - This mode instructs Cisco Catalyst Center to checks if the target devices already have the desired image version + installed. If image already present on devices, no action is taken and upgrade process is skipped for those devices. This mode + is useful for avoiding unnecessary upgrades on devices that already have the correct image version installed, thereby saving time. type: str - distributeIfNeeded: - description: DistributeIfNeeded flag. + distribute_if_needed: + description: Set the distribute_if_needed flag while activating the swim image. type: bool image_name: description: SWIM image's name @@ -252,7 +299,7 @@ tagging_details: image_name: string device_role: string - device_family_name: string + device_image_family_name: string site_name: string tagging: bool image_distribution_details: @@ -288,8 +335,7 @@ tagging_details: image_name: string device_role: string - device_family_name: string - device_type: string + device_image_family_name: string site_name: string tagging: bool @@ -308,9 +354,9 @@ - tagging_details: image_name: string device_role: string - device_type: string + device_image_family_name: string site_name: string - tagging: true + tagging: True - name: Un-tagged the given image as golden and load it on device cisco.dnac.swim_intent: @@ -327,9 +373,9 @@ - tagging_details: image_name: string device_role: string - device_type: string + device_image_family_name: string site_name: string - tagging: false + tagging: False - name: Distribute the given image on devices associated to that site with specified role. cisco.dnac.swim_intent: @@ -490,8 +536,9 @@ def site_exists(self, site_name): params={"name": site_name}, ) except Exception as e: - self.log("An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center".format(site_name), "ERROR") - self.module.fail_json(msg="Site not found") + self.msg = "An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center".format(site_name) + self.log(self.msg, "ERROR") + self.module.fail_json(msg=self.msg) if response: self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") @@ -625,7 +672,8 @@ def get_device_id(self, params): device_id = device_list[0].get("id") self.log("Device Id: {0}".format(str(device_id)), "INFO") else: - self.log("Device not found", "WARNING") + self.msg = "Device with params: '{0}' not found in Cisco Catalyst Center so can't fetch the device id".format(str(params)) + self.log(self.msg, "WARNING") return device_id @@ -707,8 +755,9 @@ def get_device_family_identifier(self, family_name): have["device_family_identifier"] = device_family_identifier self.log("Family device indentifier: {0}".format(str(device_family_identifier)), "INFO") else: - self.log("Device Family: {0} not found".format(str(family_name)), "ERROR") - self.module.fail_json(msg="Family Device Name not found", response=[]) + self.msg = "Device Family: {0} not found".format(str(family_name)) + self.log(self.msg, "ERROR") + self.module.fail_json(msg=self.msg, response=[self.msg]) self.have.update(have) def get_have(self): @@ -755,7 +804,7 @@ def get_have(self): self.have.update(have) # check if given device family name exists, store indentifier value - family_name = tagging_details.get("device_type") + family_name = tagging_details.get("device_image_family_name") self.get_device_family_identifier(family_name) if self.want.get("distribution_details"): @@ -852,13 +901,13 @@ def get_want(self, config): if config.get("import_image_details"): want["import_image"] = True want["import_type"] = config.get("import_image_details").get("type").lower() - if want["import_type"] == "url": + if want["import_type"] == "remote": want["url_import_details"] = config.get("import_image_details").get("url_details") elif want["import_type"] == "local": want["local_import_details"] = config.get("import_image_details").get("local_image_details") else: - self.log("The import type '{0}' provided is incorrect. Only 'local' or 'url' are supported.".format(want["import_type"]), "CRITICAL") - self.module.fail_json(msg="Incorrect import type. Supported Values: local or url") + self.log("The import type '{0}' provided is incorrect. Only 'local' or 'remote' are supported.".format(want["import_type"]), "CRITICAL") + self.module.fail_json(msg="Incorrect import type. Supported Values: local or remote") want["tagging_details"] = config.get("tagging_details") want["distribution_details"] = config.get("image_distribution_details") @@ -895,7 +944,7 @@ def get_diff_import(self): self.result['changed'] = False return self - if import_type == "url": + if import_type == "remote": image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") else: image_name = self.want.get("local_import_details").get("file_path") @@ -921,7 +970,7 @@ def get_diff_import(self): self.result['changed'] = False return self - if self.want.get("import_type") == "url": + if self.want.get("import_type") == "remote": import_payload_dict = {} temp_payload = self.want.get("url_import_details").get("payload")[0] keys_to_change = list(import_key_mapping.keys()) @@ -1466,7 +1515,7 @@ def verify_diff_imported(self, import_type): Verify the successful import of a software image into Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - import_type (str): The type of import, either 'url' or 'local'. + import_type (str): The type of import, either 'remote' or 'local'. Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: @@ -1476,7 +1525,7 @@ def verify_diff_imported(self, import_type): If the image does not exist, a warning message is logged indicating a potential import failure. """ - if import_type == "url": + if import_type == "remote": image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") else: image_name = self.want.get("local_import_details").get("file_path") diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index daa826daec..a2a26686ae 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -50,26 +50,26 @@ type: dict suboptions: type: - description: The source of import, supports url import or local import. + description: The source of import, supports remote import or local import. type: str local_image_details: description: Details of the local path of the image to be imported. type: dict suboptions: file_path: - description: File absolute path. + description: Give the file absolute path required while importing image from local. type: str is_third_party: - description: IsThirdParty query parameter. Third party Image check. + description: IsThirdParty query parameter. Third party Image check (Optional). type: bool third_party_application_type: - description: ThirdPartyApplicationType query parameter. Third Party Application Type. + description: ThirdPartyApplicationType query parameter. Third Party Application Type (Optional). type: str third_party_image_family: - description: ThirdPartyImageFamily query parameter. Third Party image family. + description: ThirdPartyImageFamily query parameter. Third Party image family (Optional). type: str third_party_vendor: - description: ThirdPartyVendor query parameter. Third Party Vendor. + description: ThirdPartyVendor query parameter (Optional). type: str url_details: description: URL details for SWIM import @@ -81,19 +81,20 @@ elements: dict suboptions: application_type: - description: Swim Import Via Url's applicationType. + description: Optional parameter indicating the type of application with permitted values(WLC, LINUX, FILREWALL, WINDOWS, + LOADBALANCER, THIRDPARTY etc) applicable only for third party image types. type: str image_family: - description: Swim Import Via Url's imageFamily. + description: The name of image family applicable only in case of third party images upload (Optional). type: str source_url: - description: Swim Import Image Via Url. + description: Required parameter for importing swim image via remote url. type: str is_third_party: - description: ThirdParty flag. + description: Set the boolean value if image is uploaded from third party (Optional). type: bool vendor: - description: Swim Import Via Url's vendor. + description: Name of vendor applicable only for third party image types (Optional). type: str schedule_at: description: ScheduleAt query parameter. Epoch Time (The number of milli-seconds since @@ -115,12 +116,22 @@ device_role: description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, DISTRIBUTION and CORE. + ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. + UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. + This could happen if the platform is unable to determine the device's role based on available information. + ACCESS - This role typically represents switches or access points that serve as access points for end-user devices to connect to the network. + These devices are often located at the edge of the network and provide connectivity to end-user devices. + BORDER ROUTER - These are devices that connect different network domains or segments together. They often serve as + gateways between different networks, such as connecting an enterprise network to the internet or connecting + multiple branch offices. + DISTRIBUTION - This role represents function as distribution switches or routers in hierarchical network designs. They aggregate traffic + from access switches and route it toward the core of the network or toward other distribution switches. + CORE - This role typically represents high-capacity switches or routers that form the backbone of the network. They handle large volumes + of traffic and provide connectivity between different parts of network, such as connecting distribution switches or + providing interconnection between different network segments. type: str - device_family_name: - description: Device family name(Eg Switches and Hubs) - type: str - device_type: - description: Type of the device (Eg Cisco Catalyst 9300 Switch) + device_image_family_name: + description: Device Image family name(Eg Cisco Catalyst 9300 Switch) type: str site_name: description: Site name for which SWIM image will be tagged/untagged as golden. @@ -138,11 +149,24 @@ type: dict suboptions: device_role: - description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, + description: Device Role and permissible Values are ALL, UNKNOWN, ACCESS, BORDER ROUTER, DISTRIBUTION and CORE. + ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. + UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. + This could happen if the platform is unable to determine the device's role based on available information. + ACCESS - This role typically represents switches or access points that serve as access points for end-user devices to connect to the network. + These devices are often located at the edge of the network and provide connectivity to end-user devices. + BORDER ROUTER - These are devices that connect different network domains or segments together. They often serve as + gateways between different networks, such as connecting an enterprise network to the internet or connecting + multiple branch offices. + DISTRIBUTION - This role represents function as distribution switches or routers in hierarchical network designs. They aggregate traffic + from access switches and route it toward the core of the network or toward other distribution switches. + CORE - This role typically represents high-capacity switches or routers that form the backbone of the network. They handle large volumes + of traffic and provide connectivity between different parts of network, such as connecting distribution switches or + providing interconnection between different network segments. type: str device_family_name: - description: Device family name + description: Name of the device family(Switches and Hubs etc.) type: str site_name: description: Used to get device details associated to this site. @@ -169,11 +193,11 @@ type: dict suboptions: device_role: - description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, + description: Device Role and permissible Values are ALL, UNKNOWN, ACCESS, BORDER ROUTER, DISTRIBUTION and CORE. type: str device_family_name: - description: Device family name + description: Name of the device family(Switches and Hubs etc.) type: str site_name: description: Used to get device details associated to this site. @@ -182,10 +206,20 @@ description: ActivateLowerImageVersion flag. type: bool device_upgrade_mode: - description: Swim Trigger Activation's deviceUpgradeMode. + description: It specifies the mode of upgrade to be applied to the devices having the following values - 'install', 'bundle', and 'currentlyExists'. + install - This mode instructs Cisco Catalyst Center to perform a clean installation of the new image on the target devices. + When this mode is selected, the existing image on the device is completely replaced with the new image during the upgrade process. + This ensures that the device runs only the new image version after the upgrade is completed. + bundle - This mode instructs Cisco Catalyst Center bundles the new image with the existing image on the device before initiating + the upgrade process.This mode allows for a more efficient upgrade process by preserving the existing image on the device while + adding the new image as an additional bundle.After the upgrade, the device can run either the existing image or the new bundled + image, depending on the configuration. + currentlyExists - This mode instructs Cisco Catalyst Center to checks if the target devices already have the desired image version + installed. If image already present on devices, no action is taken and upgrade process is skipped for those devices. This mode + is useful for avoiding unnecessary upgrades on devices that already have the correct image version installed, thereby saving time. type: str - distributeIfNeeded: - description: DistributeIfNeeded flag. + distribute_if_needed: + description: Set the distribute_if_needed flag while activating the swim image. type: bool image_name: description: SWIM image's name @@ -252,7 +286,7 @@ tagging_details: image_name: string device_role: string - device_family_name: string + device_image_family_name: string site_name: string tagging: bool image_distribution_details: @@ -288,8 +322,7 @@ tagging_details: image_name: string device_role: string - device_family_name: string - device_type: string + device_image_family_name: string site_name: string tagging: bool @@ -308,9 +341,9 @@ - tagging_details: image_name: string device_role: string - device_type: string + device_image_family_name: string site_name: string - tagging: true + tagging: True - name: Un-tagged the given image as golden and load it on device cisco.dnac.swim_workflow_manager: @@ -327,9 +360,9 @@ - tagging_details: image_name: string device_role: string - device_type: string + device_image_family_name: string site_name: string - tagging: false + tagging: False - name: Distribute the given image on devices associated to that site with specified role. cisco.dnac.swim_workflow_manager: @@ -489,8 +522,9 @@ def site_exists(self, site_name): params={"name": site_name}, ) except Exception as e: - self.log("An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center".format(site_name), "ERROR") - self.module.fail_json(msg="Site not found") + self.msg = "An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center".format(site_name) + self.log(self.msg, "ERROR") + self.module.fail_json(msg=self.msg) if response: self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") @@ -624,7 +658,8 @@ def get_device_id(self, params): device_id = device_list[0].get("id") self.log("Device Id: {0}".format(str(device_id)), "INFO") else: - self.log("Device not found", "WARNING") + self.msg = "Device with params: '{0}' not found in Cisco Catalyst Center so can't fetch the device id".format(str(params)) + self.log(self.msg, "WARNING") return device_id @@ -706,8 +741,9 @@ def get_device_family_identifier(self, family_name): have["device_family_identifier"] = device_family_identifier self.log("Family device indentifier: {0}".format(str(device_family_identifier)), "INFO") else: - self.log("Device Family: {0} not found".format(str(family_name)), "ERROR") - self.module.fail_json(msg="Family Device Name not found", response=[]) + self.msg = "Device Family: {0} not found".format(str(family_name)) + self.log(self.msg, "ERROR") + self.module.fail_json(msg=self.msg, response=self.msg) self.have.update(have) def get_have(self): @@ -754,7 +790,7 @@ def get_have(self): self.have.update(have) # check if given device family name exists, store indentifier value - family_name = tagging_details.get("device_type") + family_name = tagging_details.get("device_image_family_name") self.get_device_family_identifier(family_name) if self.want.get("distribution_details"): @@ -851,13 +887,13 @@ def get_want(self, config): if config.get("import_image_details"): want["import_image"] = True want["import_type"] = config.get("import_image_details").get("type").lower() - if want["import_type"] == "url": + if want["import_type"] == "remote": want["url_import_details"] = config.get("import_image_details").get("url_details") elif want["import_type"] == "local": want["local_import_details"] = config.get("import_image_details").get("local_image_details") else: - self.log("The import type '{0}' provided is incorrect. Only 'local' or 'url' are supported.".format(want["import_type"]), "CRITICAL") - self.module.fail_json(msg="Incorrect import type. Supported Values: local or url") + self.log("The import type '{0}' provided is incorrect. Only 'local' or 'remote' are supported.".format(want["import_type"]), "CRITICAL") + self.module.fail_json(msg="Incorrect import type. Supported Values: local or remote") want["tagging_details"] = config.get("tagging_details") want["distribution_details"] = config.get("image_distribution_details") @@ -894,7 +930,7 @@ def get_diff_import(self): self.result['changed'] = False return self - if import_type == "url": + if import_type == "remote": image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") else: image_name = self.want.get("local_import_details").get("file_path") @@ -920,7 +956,7 @@ def get_diff_import(self): self.result['changed'] = False return self - if self.want.get("import_type") == "url": + if self.want.get("import_type") == "remote": import_payload_dict = {} temp_payload = self.want.get("url_import_details").get("payload")[0] keys_to_change = list(import_key_mapping.keys()) @@ -1465,7 +1501,7 @@ def verify_diff_imported(self, import_type): Verify the successful import of a software image into Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - import_type (str): The type of import, either 'url' or 'local'. + import_type (str): The type of import, either 'remote' or 'local'. Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: @@ -1475,7 +1511,7 @@ def verify_diff_imported(self, import_type): If the image does not exist, a warning message is logged indicating a potential import failure. """ - if import_type == "url": + if import_type == "remote": image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") else: image_name = self.want.get("local_import_details").get("file_path") @@ -1553,7 +1589,6 @@ def verify_diff_distributed(self): Verify the distribution status of a software image in Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - import_type (str): The type of import, either 'url' or 'local'. Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: From f4ee8b975b4a82f87be86328d9d890f7f6fb75a7 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 13 Feb 2024 18:01:11 +0000 Subject: [PATCH 25/64] Changing the names of PnP type --- playbooks/PnP_Intent_Playbook.yml | 113 ++++++++++++++++++ playbooks/PnP_Workflow_Manager_Playbook.yml | 112 ++++++++++++++++++ plugins/modules/pnp_intent.py | 124 ++++++++++---------- plugins/modules/pnp_workflow_manager.py | 24 ++-- 4 files changed, 303 insertions(+), 70 deletions(-) create mode 100644 playbooks/PnP_Intent_Playbook.yml create mode 100644 playbooks/PnP_Workflow_Manager_Playbook.yml diff --git a/playbooks/PnP_Intent_Playbook.yml b/playbooks/PnP_Intent_Playbook.yml new file mode 100644 index 0000000000..72ba02f9ee --- /dev/null +++ b/playbooks/PnP_Intent_Playbook.yml @@ -0,0 +1,113 @@ +--- +- name: Manage operations - Add, claim, and delete devices of Onboarding Configuration (PnP) + hosts: localhost + connection: local + gather_facts: no + + vars_files: + - "{{ CLUSTERFILE }}" + + vars: + dnac_login: &dnac_login + dnac_host: "{{ dnac_host }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_port: "{{ dnac_port }}" + dnac_version: "{{ dnac_version }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log_level: DEBUG + + tasks: + + - name: Import devices in bulk + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: merged + config_verify: True + config: + - device_info: + - serial_number: QD2425L8M7 + state: Unclaimed + pid: c9300-24P + is_sudi_required: False + + - serial_number: QTC2320E0H9 + state: Unclaimed + pid: c9300-24P + hostname: Test-123 + + - serial_number: ETC2320E0HB + state: Unclaimed + pid: c9300-24P + + - name: Add a new device and claim it + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + device_info: + - serial_number: FJC2330E0BB + hostname: Test-9300-10 + state: Unclaimed + pid: c9300-24P + is_sudi_required: True + + - name: Claim an added Switch with template and image upgrade to a site only + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + template_name: "Ansible_PNP_Switch" + image_name: cat9k_iosxe_npe.17.03.07.SPA.bin + project_name: Onboarding Configuration + template_details: + hostname: SJC-Switch-1 + interface: TwoGigabitEthernet1/0/2 + device_info: + - serial_number: FJC271924EQ + hostname: Switch + state: Unclaimed + pid: C9300-48UXM + + - name: Claim an added Wireless Controller with template and image upgrade to a site only + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + pnp_type: catalyst_wlc + template_name: "Ansible_PNP_WLC" + image_name: C9800-40-universalk9_wlc.17.12.01.SPA.bin + template_params: + hostname: IAC-EWLC-Claimed + device_info: + - serial_number: FOX2639PAY7 + hostname: New_WLC + state: Unclaimed + pid: C9800-CL-K9 + gateway: 204.192.101.1 + ip_interface_name: TenGigabitEthernet0/0/0 + static_ip: 204.192.101.10 + subnet_mask: 255.255.255.0 + vlan_id: 1101 + + - name: Delete multiple devices from the Pnp dashboard #If device is not present it won't fail + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: deleted + config_verify: True + config: + - device_info: + - serial_number: QD2425L8M7 #Will get deleted + - serial_number: FTC2320E0HA #Doesn't exist in the inventory + - serial_number: FKC2310E0HB #Doesn't exist in the inventory + + \ No newline at end of file diff --git a/playbooks/PnP_Workflow_Manager_Playbook.yml b/playbooks/PnP_Workflow_Manager_Playbook.yml new file mode 100644 index 0000000000..0f4ee6c915 --- /dev/null +++ b/playbooks/PnP_Workflow_Manager_Playbook.yml @@ -0,0 +1,112 @@ +--- +- name: Manage operations - Add, claim, and delete devices of Onboarding Configuration (PnP) + hosts: localhost + connection: local + gather_facts: no + + vars_files: + - "{{ CLUSTERFILE }}" + + vars: + dnac_login: &dnac_login + dnac_host: "{{ dnac_host }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_port: "{{ dnac_port }}" + dnac_version: "{{ dnac_version }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log_level: DEBUG + + tasks: + + - name: Import devices in bulk + cisco.dnac.pnp_workflow_manager: + <<: *dnac_login + dnac_log: True + state: merged + config_verify: True + config: + - device_info: + - serial_number: QD2425L8M7 + state: Unclaimed + pid: c9300-24P + is_sudi_required: False + + - serial_number: QTC2320E0H9 + state: Unclaimed + pid: c9300-24P + hostname: Test-123 + + - serial_number: ETC2320E0HB + state: Unclaimed + pid: c9300-24P + + - name: Add a new device and claim it + cisco.dnac.pnp_workflow_manager: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + device_info: + - serial_number: FJC2330E0BB + hostname: Test-9300-10 + state: Unclaimed + pid: c9300-24P + is_sudi_required: True + + - name: Claim an added Switch with template and image upgrade to a site only + cisco.dnac.pnp_workflow_manager: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + template_name: "Ansible_PNP_Switch" + image_name: cat9k_iosxe_npe.17.03.07.SPA.bin + project_name: Onboarding Configuration + template_details: + hostname: SJC-Switch-1 + interface: TwoGigabitEthernet1/0/2 + device_info: + - serial_number: FJC271924EQ + hostname: Switch + state: Unclaimed + pid: C9300-48UXM + + - name: Claim an added Wireless Controller with template and image upgrade to a site only + cisco.dnac.pnp_workflow_manager: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + pnp_type: catalyst_wlc + template_name: "Ansible_PNP_WLC" + image_name: C9800-40-universalk9_wlc.17.12.01.SPA.bin + template_params: + hostname: IAC-EWLC-Claimed + device_info: + - serial_number: FOX2639PAY7 + hostname: New_WLC + state: Unclaimed + pid: C9800-CL-K9 + gateway: 204.192.101.1 + ip_interface_name: TenGigabitEthernet0/0/0 + static_ip: 204.192.101.10 + subnet_mask: 255.255.255.0 + vlan_id: 1101 + + - name: Delete multiple devices from the Pnp dashboard #If device is not present it won't fail + cisco.dnac.pnp_workflow_manager: + <<: *dnac_login + dnac_log: True + state: deleted + config_verify: True + config: + - device_info: + - serial_number: QD2425L8M7 #Will get deleted + - serial_number: FTC2320E0HA #Doesn't exist in the inventory + - serial_number: FKC2310E0HB #Doesn't exist in the inventory + diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 85436a1cf9..8af92f1df5 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -1,12 +1,12 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright (c) 2022, Cisco Systems +# Copyright (c) 2024, Cisco Systems # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type -__author__ = ("Madhan Sankaranarayanan, Rishita Chowdhary, Abinash Mishra") +__author__ = ("Abinash Mishra, Madhan Sankaranarayanan, Rishita Chowdhary") DOCUMENTATION = r""" --- @@ -20,16 +20,16 @@ version_added: '6.6.0' extends_documentation_fragment: - cisco.dnac.intent_params -author: Madhan Sankaranarayanan (@madhansansel) +author: Abinash Mishra (@abimishr) + Madhan Sankaranarayanan (@madhansansel) Rishita Chowdhary (@rishitachowdhary) - Abinash Mishra (@abimishr) options: config_verify: - description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config. type: bool default: False state: - description: The state of DNAC after module completion. + description: The state of Cisco Catalyst Center after module completion. type: str choices: [ merged, deleted ] default: merged @@ -62,7 +62,10 @@ type: str default: Onboarding Configuration pnp_type: - description: Device type of the Pnp device (Default/catalyst_wlc/access_point/stack_switch) + description: Device type of the Pnp device (Default/CatalystWLC/AccessPoint/StackSwitch). Default can be + used for a switch or a router. CatalystWLC should be used for 9800 series wireless Controllers. AccessPoint should + be used while claiming an AP. StackSwitch must be used for a bundle of switches ating as a single switch and is available + on the ACCESS layer. type: str default: Default static_ip: @@ -78,7 +81,8 @@ description: Vlan Id allocated for claimimg of Wireless Controller type: str ip_interface_name: - description: Name of the Interface used for Pnp by the Wireless Controller + description: Name of the Interface used for Pnp by the Wireless Controller. It should be configured on the Controller + before claiming. type: str rf_profile: description: Radio frequecy profile of the AP being claimed (HIGH/LOW/TYPICAL) @@ -178,7 +182,7 @@ RETURN = r""" #Case_1: When the device is claimed successfully. response_1: - description: A dictionary with the response returned by the Cisco DNAC Python SDK + description: A dictionary with the response returned by the Cisco Catalyst Center Python SDK returned: always type: dict sample: > @@ -193,7 +197,7 @@ #Case_2: Given site/image/template/project not found or Device is not found for deletion response_2: - description: A list with the response returned by the Cisco DNAC Python SDK + description: A list with the response returned by the Cisco Catalyst Center Python SDK returned: always type: list sample: > @@ -204,7 +208,7 @@ #Case_3: Error while deleting/claiming a device response_3: - description: A string with the response returned by the Cisco DNAC Python SDK + description: A string with the response returned by the Cisco Catalyst Center Python SDK returned: always type: dict sample: > @@ -221,7 +225,7 @@ ) -class DnacPnp(DnacBase): +class PnP(DnacBase): def __init__(self, module): super().__init__(module) @@ -374,7 +378,7 @@ def get_site_type(self): def get_pnp_params(self, params): """ - Store pnp parameters from the playbook for pnp processing in DNAC. + Store pnp parameters from the playbook for pnp processing in Cisco Catalyst Center. Parameters: - self: The instance of the class containing the 'config' @@ -478,16 +482,14 @@ def get_claim_params(self): 'configInfo': configinfo, } - if claim_params["type"] == "catalyst_wlc": - claim_params["type"] = "CatalystWLC" + if claim_params["type"] == "CatalystWLC": claim_params["staticIP"] = self.validated_config[0]['static_ip'] claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] claim_params["gateway"] = self.validated_config[0]['gateway'] claim_params["vlanId"] = str(self.validated_config[0]['vlan_id']) claim_params["ipInterfaceName"] = self.validated_config[0]['ip_interface_name'] - if claim_params["type"] == "access_point": - claim_params["type"] = "AccessPoint" + if claim_params["type"] == "AccessPoint": claim_params["rfProfile"] = self.validated_config[0]["rf_profile"] self.log("Paramters used for claiming are {0}".format(str(claim_params)), "INFO") @@ -534,7 +536,7 @@ def get_reset_params(self): def get_have(self): """ - Get the current image, template and site details from the DNAC. + Get the current image, template and site details from the Cisco Catalyst Center. Parameters: - self: The instance of the class containing the 'config' attribute @@ -615,7 +617,7 @@ def get_have(self): if site_exists: have["site_id"] = site_id self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") - if self.want.get("pnp_type") == "access_point": + if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": self.msg = "The site type must be specified as 'floor'\ for claiming an AP" @@ -661,7 +663,7 @@ def get_have(self): return self self.msg = "Successfully collected all project and template \ - parameters from dnac for comparison" + parameters from Cisco Catalyst Center for comparison" self.log(self.msg, "INFO") self.status = "success" self.have = have @@ -670,7 +672,7 @@ def get_have(self): def get_want(self, config): """ Get all the image, template and site and pnp related - information from playbook that is needed to be created in DNAC. + information from playbook that is needed to be created in Cisco Catalyst Center. Parameters: - self: The instance of the class containing the 'config' @@ -705,14 +707,14 @@ def get_want(self, config): get("hostname") ) - if self.want["pnp_type"] == "catalyst_wlc": + if self.want["pnp_type"] == "CatalystWLC": self.want["static_ip"] = config.get('static_ip') self.want["subnet_mask"] = config.get('subnet_mask') self.want["gateway"] = config.get('gateway') self.want["vlan_id"] = config.get('vlan_id') self.want["ip_interface_name"] = config.get('ip_interface_name') - elif self.want["pnp_type"] == "access_point": + elif self.want["pnp_type"] == "AccessPoint": self.want["rf_profile"] = config.get("rf_profile") self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" @@ -726,7 +728,7 @@ def get_diff_merged(self): If given device doesnot exist then add it to pnp database and get the device id Args: - self: An instance of a class used for interacting with Cisco DNA Center. + self: An instance of a class used for interacting with Cisco Catalyst Center. Returns: object: An instance of the class with updated results and status based on the processing of differences. Based on the length of devices passed @@ -734,7 +736,7 @@ def get_diff_merged(self): Description: The function processes the differences and, depending on the changes required, it may add, update,or resynchronize devices in - Cisco DNA Center. The updated results and status are stored in the + Cisco Catalyst Center. The updated results and status are stored in the class instance for further use. """ @@ -912,7 +914,7 @@ class instance for further use. reset_paramters = self.get_reset_params() reset_response = self.dnac_apply['exec']( family="device_onboarding_pnp", - function="update_device", + function="reset_device", params={"payload": reset_paramters}, op_modifies=True, ) @@ -923,6 +925,8 @@ class instance for further use. self.result['diff'] = self.validated_config self.result['changed'] = True + return self + if not ( prov_dev_response.get("response") == 0 and plan_dev_response.get("response") == 0 and @@ -960,14 +964,14 @@ def get_diff_deleted(self): and is in unclaimed or failed state delete the given device Args: - self: An instance of a class used for interacting with Cisco DNA Center. + self: An instance of a class used for interacting with Cisco Catalyst Center. Here we pass a list of device info to be deleted Returns: self: An instance of the class with updated results and status based on the deletion operation. It tells us the number of devices deleted if any of the devices get deleted Description: - This function is responsible for removing devices from the Cisco DNA Center PnP GUI and + This function is responsible for removing devices from the Cisco Catalyst Center PnP GUI and pass new changes if devices are already deleted. """ devices_deleted = [] @@ -1013,14 +1017,14 @@ def get_diff_deleted(self): def verify_diff_merged(self, config): """ - Verify the merged status(Creation/Updation) of PnP configuration in Cisco DNA Center. + Verify the merged status(Creation/Updation) of PnP configuration in Cisco Catalyst Center. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): The configuration details to be verified. Return: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method checks the merged status of a configuration in Cisco DNA Center by + This method checks the merged status of a configuration in Cisco Catalyst Center by retrieving the current state (have) and desired state (want) of the configuration, logs the states, and validates whether the specified device(s) exists in the DNA Center configuration's PnP Database. @@ -1028,7 +1032,7 @@ def verify_diff_merged(self, config): self.log("Current State (have): {0}".format(str(self.have)), "INFO") self.log("Desired State (want): {0}".format(str(config)), "INFO") - # Code to validate dnac config for merged state + # Code to validate Cisco Catalyst Center config for merged state for device in self.want.get("pnp_params"): device_response = self.dnac_apply['exec']( family="device_onboarding_pnp", @@ -1038,14 +1042,14 @@ def verify_diff_merged(self, config): if (device_response and (len(device_response) == 1)): msg = ( "Requested Device with Serial No. {0} is " - "present in Cisco DNA Center and" + "present in Cisco Catalyst Center and" " addition verified.".format(device["deviceInfo"]["serialNumber"])) self.log(msg, "INFO") else: msg = ( "Requested Device with Serial No. {0} is " - "not present in Cisco DNA " + "not present in Cisco Catalyst Center" "Center".format(device["deviceInfo"]["serialNumber"])) self.log(msg, "WARNING") @@ -1054,21 +1058,21 @@ def verify_diff_merged(self, config): def verify_diff_deleted(self, config): """ - Verify the deletion status of PnP configuration in Cisco DNA Center. + Verify the deletion status of PnP configuration in Cisco Catalyst Center. Args: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): The configuration details to be verified. Return: - - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - This method checks the deletion status of a configuration in Cisco DNA Center. - It validates whether the specified device(s) exists in the DNA Center configuration's + This method checks the deletion status of a configuration in Cisco Catalyst Center. + It validates whether the specified device(s) exists in the Cisco Catalyst Center configuration's PnP Database. """ self.log("Current State (have): {0}".format(str(self.have)), "INFO") self.log("Desired State (want): {0}".format(str(config)), "INFO") - # Code to validate dnac config for deleted state + # Code to validate Cisco Catalyst Center config for deleted state for device in self.want.get("pnp_params"): device_response = self.dnac_apply['exec']( family="device_onboarding_pnp", @@ -1085,7 +1089,7 @@ def verify_diff_deleted(self, config): else: msg = ( "Requested Device with Serial No. {0} is " - "present in Cisco DNA Center".format(device["deviceInfo"]["serialNumber"])) + "present in Cisco Catalyst Center".format(device["deviceInfo"]["serialNumber"])) self.log(msg, "WARNING") self.status = "success" @@ -1116,26 +1120,26 @@ def main(): module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) - dnac_pnp = DnacPnp(module) - - state = dnac_pnp.params.get("state") - if state not in dnac_pnp.supported_states: - dnac_pnp.status = "invalid" - dnac_pnp.msg = "State {0} is invalid".format(state) - dnac_pnp.check_return_status() - - dnac_pnp.validate_input().check_return_status() - config_verify = dnac_pnp.params.get("config_verify") - - for config in dnac_pnp.validated_config: - dnac_pnp.reset_values() - dnac_pnp.get_want(config).check_return_status() - dnac_pnp.get_have().check_return_status() - dnac_pnp.get_diff_state_apply[state]().check_return_status() + ccc_pnp = PnP(module) + + state = ccc_pnp.params.get("state") + if state not in ccc_pnp.supported_states: + ccc_pnp.status = "invalid" + ccc_pnp.msg = "State {0} is invalid".format(state) + ccc_pnp.check_return_status() + + ccc_pnp.validate_input().check_return_status() + config_verify = ccc_pnp.params.get("config_verify") + + for config in ccc_pnp.validated_config: + ccc_pnp.reset_values() + ccc_pnp.get_want(config).check_return_status() + ccc_pnp.get_have().check_return_status() + ccc_pnp.get_diff_state_apply[state]().check_return_status() if config_verify: - dnac_pnp.verify_diff_state_apply[state](config).check_return_status() + ccc_pnp.verify_diff_state_apply[state](config).check_return_status() - module.exit_json(**dnac_pnp.result) + module.exit_json(**ccc_pnp.result) if __name__ == '__main__': diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index fb9e35f918..e0aba8ca44 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -62,7 +62,10 @@ type: str default: Onboarding Configuration pnp_type: - description: Device type of the Pnp device (Default/catalyst_wlc/access_point/stack_switch) + description: Device type of the Pnp device (Default/CatalystWLC/AccessPoint/StackSwitch). Default can be + used for a switch or a router. CatalystWLC should be used for 9800 series wireless Controllers. AccessPoint should + be used while claiming an AP. StackSwitch must be used for a bundle of switches ating as a single switch and is available + on the ACCESS layer. type: str default: Default static_ip: @@ -78,7 +81,8 @@ description: Vlan Id allocated for claimimg of Wireless Controller type: str ip_interface_name: - description: Name of the Interface used for Pnp by the Wireless Controller + description: Name of the Interface used for Pnp by the Wireless Controller. It should be configured on the Controller + before claiming. type: str rf_profile: description: Radio frequecy profile of the AP being claimed (HIGH/LOW/TYPICAL) @@ -478,16 +482,14 @@ def get_claim_params(self): 'configInfo': configinfo, } - if claim_params["type"] == "catalyst_wlc": - claim_params["type"] = "CatalystWLC" + if claim_params["type"] == "CatalystWLC": claim_params["staticIP"] = self.validated_config[0]['static_ip'] claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] claim_params["gateway"] = self.validated_config[0]['gateway'] claim_params["vlanId"] = str(self.validated_config[0]['vlan_id']) claim_params["ipInterfaceName"] = self.validated_config[0]['ip_interface_name'] - if claim_params["type"] == "access_point": - claim_params["type"] = "AccessPoint" + if claim_params["type"] == "AccessPoint": claim_params["rfProfile"] = self.validated_config[0]["rf_profile"] self.log("Paramters used for claiming are {0}".format(str(claim_params)), "INFO") @@ -615,7 +617,7 @@ def get_have(self): if site_exists: have["site_id"] = site_id self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") - if self.want.get("pnp_type") == "access_point": + if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": self.msg = "The site type must be specified as 'floor'\ for claiming an AP" @@ -705,14 +707,14 @@ def get_want(self, config): get("hostname") ) - if self.want["pnp_type"] == "catalyst_wlc": + if self.want["pnp_type"] == "CatalystWLC": self.want["static_ip"] = config.get('static_ip') self.want["subnet_mask"] = config.get('subnet_mask') self.want["gateway"] = config.get('gateway') self.want["vlan_id"] = config.get('vlan_id') self.want["ip_interface_name"] = config.get('ip_interface_name') - elif self.want["pnp_type"] == "access_point": + elif self.want["pnp_type"] == "AccessPoint": self.want["rf_profile"] = config.get("rf_profile") self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" @@ -912,7 +914,7 @@ class instance for further use. reset_paramters = self.get_reset_params() reset_response = self.dnac_apply['exec']( family="device_onboarding_pnp", - function="update_device", + function="reset_device", params={"payload": reset_paramters}, op_modifies=True, ) @@ -923,6 +925,8 @@ class instance for further use. self.result['diff'] = self.validated_config self.result['changed'] = True + return self + if not ( prov_dev_response.get("response") == 0 and plan_dev_response.get("response") == 0 and From 1f0b30841496d4637c4e3af4a0a374caba9356b2 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 13 Feb 2024 18:07:09 +0000 Subject: [PATCH 26/64] Changing the names of PnP type --- playbooks/PnP_Intent_Playbook.yml | 113 -------------------- playbooks/PnP_Workflow_Manager_Playbook.yml | 2 +- 2 files changed, 1 insertion(+), 114 deletions(-) delete mode 100644 playbooks/PnP_Intent_Playbook.yml diff --git a/playbooks/PnP_Intent_Playbook.yml b/playbooks/PnP_Intent_Playbook.yml deleted file mode 100644 index 72ba02f9ee..0000000000 --- a/playbooks/PnP_Intent_Playbook.yml +++ /dev/null @@ -1,113 +0,0 @@ ---- -- name: Manage operations - Add, claim, and delete devices of Onboarding Configuration (PnP) - hosts: localhost - connection: local - gather_facts: no - - vars_files: - - "{{ CLUSTERFILE }}" - - vars: - dnac_login: &dnac_login - dnac_host: "{{ dnac_host }}" - dnac_username: "{{ dnac_username }}" - dnac_password: "{{ dnac_password }}" - dnac_verify: "{{ dnac_verify }}" - dnac_port: "{{ dnac_port }}" - dnac_version: "{{ dnac_version }}" - dnac_debug: "{{ dnac_debug }}" - dnac_log_level: DEBUG - - tasks: - - - name: Import devices in bulk - cisco.dnac.pnp_intent: - <<: *dnac_login - dnac_log: True - state: merged - config_verify: True - config: - - device_info: - - serial_number: QD2425L8M7 - state: Unclaimed - pid: c9300-24P - is_sudi_required: False - - - serial_number: QTC2320E0H9 - state: Unclaimed - pid: c9300-24P - hostname: Test-123 - - - serial_number: ETC2320E0HB - state: Unclaimed - pid: c9300-24P - - - name: Add a new device and claim it - cisco.dnac.pnp_intent: - <<: *dnac_login - dnac_log: True - state: merged - config: - - site_name: Global/USA/San Francisco/BGL_18 - device_info: - - serial_number: FJC2330E0BB - hostname: Test-9300-10 - state: Unclaimed - pid: c9300-24P - is_sudi_required: True - - - name: Claim an added Switch with template and image upgrade to a site only - cisco.dnac.pnp_intent: - <<: *dnac_login - dnac_log: True - state: merged - config: - - site_name: Global/USA/San Francisco/BGL_18 - template_name: "Ansible_PNP_Switch" - image_name: cat9k_iosxe_npe.17.03.07.SPA.bin - project_name: Onboarding Configuration - template_details: - hostname: SJC-Switch-1 - interface: TwoGigabitEthernet1/0/2 - device_info: - - serial_number: FJC271924EQ - hostname: Switch - state: Unclaimed - pid: C9300-48UXM - - - name: Claim an added Wireless Controller with template and image upgrade to a site only - cisco.dnac.pnp_intent: - <<: *dnac_login - dnac_log: True - state: merged - config: - - site_name: Global/USA/San Francisco/BGL_18 - pnp_type: catalyst_wlc - template_name: "Ansible_PNP_WLC" - image_name: C9800-40-universalk9_wlc.17.12.01.SPA.bin - template_params: - hostname: IAC-EWLC-Claimed - device_info: - - serial_number: FOX2639PAY7 - hostname: New_WLC - state: Unclaimed - pid: C9800-CL-K9 - gateway: 204.192.101.1 - ip_interface_name: TenGigabitEthernet0/0/0 - static_ip: 204.192.101.10 - subnet_mask: 255.255.255.0 - vlan_id: 1101 - - - name: Delete multiple devices from the Pnp dashboard #If device is not present it won't fail - cisco.dnac.pnp_intent: - <<: *dnac_login - dnac_log: True - state: deleted - config_verify: True - config: - - device_info: - - serial_number: QD2425L8M7 #Will get deleted - - serial_number: FTC2320E0HA #Doesn't exist in the inventory - - serial_number: FKC2310E0HB #Doesn't exist in the inventory - - \ No newline at end of file diff --git a/playbooks/PnP_Workflow_Manager_Playbook.yml b/playbooks/PnP_Workflow_Manager_Playbook.yml index 0f4ee6c915..85436b78cb 100644 --- a/playbooks/PnP_Workflow_Manager_Playbook.yml +++ b/playbooks/PnP_Workflow_Manager_Playbook.yml @@ -82,7 +82,7 @@ state: merged config: - site_name: Global/USA/San Francisco/BGL_18 - pnp_type: catalyst_wlc + pnp_type: CatalystWLC template_name: "Ansible_PNP_WLC" image_name: C9800-40-universalk9_wlc.17.12.01.SPA.bin template_params: From 77a84e7b6ea2ab41ac47fa75bea6e8969ac0c327 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 13 Feb 2024 18:12:42 +0000 Subject: [PATCH 27/64] Changing the names of PnP type --- plugins/modules/pnp_intent.py | 6 +++--- plugins/modules/pnp_workflow_manager.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 8af92f1df5..20e88f54ef 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -63,9 +63,9 @@ default: Onboarding Configuration pnp_type: description: Device type of the Pnp device (Default/CatalystWLC/AccessPoint/StackSwitch). Default can be - used for a switch or a router. CatalystWLC should be used for 9800 series wireless Controllers. AccessPoint should - be used while claiming an AP. StackSwitch must be used for a bundle of switches ating as a single switch and is available - on the ACCESS layer. + used for a switch or a router. CatalystWLC should be used for 9800 series wireless Controllers. AccessPoint should + be used while claiming an AP. StackSwitch must be used for a bundle of switches ating as a single switch and is available + on the ACCESS layer. type: str default: Default static_ip: diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index e0aba8ca44..fbded74a10 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -63,9 +63,9 @@ default: Onboarding Configuration pnp_type: description: Device type of the Pnp device (Default/CatalystWLC/AccessPoint/StackSwitch). Default can be - used for a switch or a router. CatalystWLC should be used for 9800 series wireless Controllers. AccessPoint should - be used while claiming an AP. StackSwitch must be used for a bundle of switches ating as a single switch and is available - on the ACCESS layer. + used for a switch or a router. CatalystWLC should be used for 9800 series wireless Controllers. AccessPoint should + be used while claiming an AP. StackSwitch must be used for a bundle of switches ating as a single switch and is available + on the ACCESS layer. type: str default: Default static_ip: From eef65505f13050cf85fce47f83f7f4fdfda89877 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 13 Feb 2024 18:17:19 +0000 Subject: [PATCH 28/64] Changing the names of PnP type --- playbooks/PnP_Workflow_Manager_Playbook.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/playbooks/PnP_Workflow_Manager_Playbook.yml b/playbooks/PnP_Workflow_Manager_Playbook.yml index 85436b78cb..e671022d36 100644 --- a/playbooks/PnP_Workflow_Manager_Playbook.yml +++ b/playbooks/PnP_Workflow_Manager_Playbook.yml @@ -109,4 +109,3 @@ - serial_number: QD2425L8M7 #Will get deleted - serial_number: FTC2320E0HA #Doesn't exist in the inventory - serial_number: FKC2310E0HB #Doesn't exist in the inventory - From 1c5934523061d4d3398a5d58329e4b38b1a32fc6 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 14 Feb 2024 09:52:16 +0530 Subject: [PATCH 29/64] replace boolean false value to False in swim workflow manager playbook --- playbooks/swim_workflow_manager.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/swim_workflow_manager.yml b/playbooks/swim_workflow_manager.yml index 29bc027374..0bd1ad2815 100644 --- a/playbooks/swim_workflow_manager.yml +++ b/playbooks/swim_workflow_manager.yml @@ -31,7 +31,7 @@ site_name: "{{item.site_name}}" device_role: "{{ item.device_role }}" device_image_family_name: "{{ item.device_image_family_name }}" - tagging: false + tagging: False image_distribution_details: image_name: "{{item.image_name}}" site_name: "{{item.site_name}}" From e646929b3abcb06bde229b6910ebaf4f0545bc25 Mon Sep 17 00:00:00 2001 From: Abinash Date: Wed, 14 Feb 2024 09:14:56 +0000 Subject: [PATCH 30/64] Changing the names of PnP type --- playbooks/PnP.yml | 9 ++-- playbooks/PnP_Workflow_Manager_Playbook.yml | 6 +-- plugins/modules/pnp_intent.py | 46 ++++++++++++++------- plugins/modules/pnp_workflow_manager.py | 46 ++++++++++++++------- 4 files changed, 68 insertions(+), 39 deletions(-) diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml index b034bc3f81..250c2a7462 100644 --- a/playbooks/PnP.yml +++ b/playbooks/PnP.yml @@ -56,7 +56,7 @@ pid: c9300-24P is_sudi_required: True - - name: Claim an added Switch with template and image upgrade to a site only + - name: Claim a pre-added switch, apply a template, and perform an image upgrade for a specific site cisco.dnac.pnp_intent: <<: *dnac_login dnac_log: True @@ -75,14 +75,14 @@ state: Unclaimed pid: C9300-48UXM - - name: Claim an added Wireless Controller with template and image upgrade to a site only + - name: Claim an existing Wireless Controller, apply a template, and upgrade its image for a specified site cisco.dnac.pnp_intent: <<: *dnac_login dnac_log: True state: merged config: - site_name: Global/USA/San Francisco/BGL_18 - pnp_type: catalyst_wlc + pnp_type: CatalystWLC template_name: "Ansible_PNP_WLC" image_name: C9800-40-universalk9_wlc.17.12.01.SPA.bin template_params: @@ -98,7 +98,7 @@ subnet_mask: 255.255.255.0 vlan_id: 1101 - - name: Delete multiple devices from the Pnp dashboard #If device is not present it won't fail + - name: Remove multiple devices from the PnP dashboard safely (ignores non-existent devices) cisco.dnac.pnp_intent: <<: *dnac_login dnac_log: True @@ -109,3 +109,4 @@ - serial_number: QD2425L8M7 #Will get deleted - serial_number: FTC2320E0HA #Doesn't exist in the inventory - serial_number: FKC2310E0HB #Doesn't exist in the inventory + diff --git a/playbooks/PnP_Workflow_Manager_Playbook.yml b/playbooks/PnP_Workflow_Manager_Playbook.yml index e671022d36..846ebf3a72 100644 --- a/playbooks/PnP_Workflow_Manager_Playbook.yml +++ b/playbooks/PnP_Workflow_Manager_Playbook.yml @@ -56,7 +56,7 @@ pid: c9300-24P is_sudi_required: True - - name: Claim an added Switch with template and image upgrade to a site only + - name: Claim a pre-added switch, apply a template, and perform an image upgrade for a specific site cisco.dnac.pnp_workflow_manager: <<: *dnac_login dnac_log: True @@ -75,7 +75,7 @@ state: Unclaimed pid: C9300-48UXM - - name: Claim an added Wireless Controller with template and image upgrade to a site only + - name: Claim an existing Wireless Controller, apply a template, and upgrade its image for a specified site cisco.dnac.pnp_workflow_manager: <<: *dnac_login dnac_log: True @@ -98,7 +98,7 @@ subnet_mask: 255.255.255.0 vlan_id: 1101 - - name: Delete multiple devices from the Pnp dashboard #If device is not present it won't fail + - name: Remove multiple devices from the PnP dashboard safely (ignores non-existent devices) cisco.dnac.pnp_workflow_manager: <<: *dnac_login dnac_log: True diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 20e88f54ef..2592e5335f 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -62,12 +62,15 @@ type: str default: Onboarding Configuration pnp_type: - description: Device type of the Pnp device (Default/CatalystWLC/AccessPoint/StackSwitch). Default can be - used for a switch or a router. CatalystWLC should be used for 9800 series wireless Controllers. AccessPoint should - be used while claiming an AP. StackSwitch must be used for a bundle of switches ating as a single switch and is available - on the ACCESS layer. + description: Specifies the device type for the Plug and Play (PnP) device. + - Options include 'Default', 'CatalystWLC', 'AccessPoint', or 'StackSwitch'. + - 'Default' is applicable to switches and routers. + - 'CatalystWLC' should be selected for 9800 series wireless controllers. + - 'AccessPoint' is used when claiming an access point. + - 'StackSwitch' should be chosen for a group of switches that operate as a single switch, typically used in the access layer. type: str - default: Default + choices: [ 'Default', 'CatalystWLC', 'AccessPoint', 'StackSwitch' ] + default: 'Default' static_ip: description: Management IP address of the Wireless Controller type: str @@ -81,28 +84,39 @@ description: Vlan Id allocated for claimimg of Wireless Controller type: str ip_interface_name: - description: Name of the Interface used for Pnp by the Wireless Controller. It should be configured on the Controller - before claiming. + description: Specifies the interface name utilized for Plug and Play (PnP) by the Wireless Controller. + Ensure this interface is pre-configured on the Controller prior to device claiming. type: str rf_profile: - description: Radio frequecy profile of the AP being claimed (HIGH/LOW/TYPICAL) + description: + - Radio Frequecy (RF) profile of the AP being claimed. + - RF Profiles allow you to tune groups of APs that share a common coverage zone together. + - They selectively change how Radio Resource Management will operate the APs within that coverage zone. + - HIGH RF profile allows you to use more power and allows to join AP with the client in an easier fashion. + - TYPICAL RF profile is a blend of moderate power and moderate visibility to the client. + - LOW RF profile allows you to consume lesser power and has least visibility to the client. type: str + choices: [ 'HIGH', 'LOW', 'TYPICAL' ] device_info: - description: Pnp Device's device_info. This is mainly for adding the devices that are - not a part of the PnP database. For single addition the length of the list must be equal to one. - Followed by single addition a device can be claimed as well if site name is provided. - For Bulk Import of devices the size of the list must be greater than 1 and can be only used for adding. - For claiming the devices please use separate tasks or configs in the case of bulk import. + description: + - Provides the device-specific information required for adding devices to the PnP database that are not already present. + - For adding a single device, the list should contain exactly one set of device information. If a site name is also provided, + the device can be claimed immediately after being added. + - For bulk import, the list must contain information for more than one device. Bulk import is intended solely for adding devices; + claiming must be performed with separate tasks or configurations. type: list required: true elements: dict suboptions: hostname: - description: Pnp Device's hostname that we want to keep post claiming. Hostname can only - be changed during claiming not bulk adding/ single adding + description: + - Defines the desired hostname for the PnP device after it has been claimed. + - The hostname can only be assigned or changed during the claim process, not during bulk or single device additions. type: str state: - description: Pnp Device's onbording state (Unclaimed/Claimed/Provisioned). + description: + - Represents the onboarding state of the PnP device. + - Possible values are 'Unclaimed', 'Claimed', or 'Provisioned'. type: str pid: description: Pnp Device's pid. diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index fbded74a10..2081f2bb79 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -62,12 +62,15 @@ type: str default: Onboarding Configuration pnp_type: - description: Device type of the Pnp device (Default/CatalystWLC/AccessPoint/StackSwitch). Default can be - used for a switch or a router. CatalystWLC should be used for 9800 series wireless Controllers. AccessPoint should - be used while claiming an AP. StackSwitch must be used for a bundle of switches ating as a single switch and is available - on the ACCESS layer. + description: Specifies the device type for the Plug and Play (PnP) device. + - Options include 'Default', 'CatalystWLC', 'AccessPoint', or 'StackSwitch'. + - 'Default' is applicable to switches and routers. + - 'CatalystWLC' should be selected for 9800 series wireless controllers. + - 'AccessPoint' is used when claiming an access point. + - 'StackSwitch' should be chosen for a group of switches that operate as a single switch, typically used in the access layer. type: str - default: Default + choices: [ 'Default', 'CatalystWLC', 'AccessPoint', 'StackSwitch' ] + default: 'Default' static_ip: description: Management IP address of the Wireless Controller type: str @@ -81,28 +84,39 @@ description: Vlan Id allocated for claimimg of Wireless Controller type: str ip_interface_name: - description: Name of the Interface used for Pnp by the Wireless Controller. It should be configured on the Controller - before claiming. + description: Specifies the interface name utilized for Plug and Play (PnP) by the Wireless Controller. + Ensure this interface is pre-configured on the Controller prior to device claiming. type: str rf_profile: - description: Radio frequecy profile of the AP being claimed (HIGH/LOW/TYPICAL) + description: + - Radio Frequecy (RF) profile of the AP being claimed. + - RF Profiles allow you to tune groups of APs that share a common coverage zone together. + - They selectively change how Radio Resource Management will operate the APs within that coverage zone. + - HIGH RF profile allows you to use more power and allows to join AP with the client in an easier fashion. + - TYPICAL RF profile is a blend of moderate power and moderate visibility to the client. + - LOW RF profile allows you to consume lesser power and has least visibility to the client. type: str + choices: [ 'HIGH', 'LOW', 'TYPICAL' ] device_info: - description: Pnp Device's device_info. This is mainly for adding the devices that are - not a part of the PnP database. For single addition the length of the list must be equal to one. - Followed by single addition a device can be claimed as well if site name is provided. - For Bulk Import of devices the size of the list must be greater than 1 and can be only used for adding. - For claiming the devices please use separate tasks or configs in the case of bulk import. + description: + - Provides the device-specific information required for adding devices to the PnP database that are not already present. + - For adding a single device, the list should contain exactly one set of device information. If a site name is also provided, + the device can be claimed immediately after being added. + - For bulk import, the list must contain information for more than one device. Bulk import is intended solely for adding devices; + claiming must be performed with separate tasks or configurations. type: list required: true elements: dict suboptions: hostname: - description: Pnp Device's hostname that we want to keep post claiming. Hostname can only - be changed during claiming not bulk adding/ single adding + description: + - Defines the desired hostname for the PnP device after it has been claimed. + - The hostname can only be assigned or changed during the claim process, not during bulk or single device additions. type: str state: - description: Pnp Device's onbording state (Unclaimed/Claimed/Provisioned). + description: + - Represents the onboarding state of the PnP device. + - Possible values are 'Unclaimed', 'Claimed', or 'Provisioned'. type: str pid: description: Pnp Device's pid. From 6f741a4cc0d8d45075048b6f595419e432c38f56 Mon Sep 17 00:00:00 2001 From: Abinash Date: Wed, 14 Feb 2024 09:18:56 +0000 Subject: [PATCH 31/64] Changing the names of PnP type --- playbooks/PnP.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml index 250c2a7462..63bad68e09 100644 --- a/playbooks/PnP.yml +++ b/playbooks/PnP.yml @@ -108,5 +108,4 @@ - device_info: - serial_number: QD2425L8M7 #Will get deleted - serial_number: FTC2320E0HA #Doesn't exist in the inventory - - serial_number: FKC2310E0HB #Doesn't exist in the inventory - + - serial_number: FKC2310E0HB #Doesn't exist in the inventory \ No newline at end of file From 70aac58334e90e243c41812e73ddd78537825297 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 14 Feb 2024 17:41:50 +0530 Subject: [PATCH 32/64] Added detailed description in the documentation, solved the ipv4 gateway bug --- plugins/modules/network_settings_intent.py | 86 +++++++++++++------ .../network_settings_workflow_manager.py | 82 ++++++++++++------ 2 files changed, 114 insertions(+), 54 deletions(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index bb3ab1d6de..a0fe75e9b5 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -26,11 +26,11 @@ Madhan Sankaranarayanan (@madhansansel) options: config_verify: - description: Set to True to verify the Cisco DNA Center after applying the playbook config. + description: Set to True to verify the Cisco Catalyst Center after applying the playbook config. type: bool default: False state: - description: The state of Cisco DNA Center after module completion. + description: The state of Cisco Catalyst Center after module completion. type: str choices: [ merged, deleted ] default: merged @@ -42,7 +42,7 @@ required: true suboptions: global_pool_details: - description: Global ip pool manages IPv4 and IPv6 IP pools. + description: Manages IPv4 and IPv6 IP pools in the global level. type: dict suboptions: settings: @@ -50,38 +50,54 @@ type: dict suboptions: ip_pool: - description: Global Pool's ippool. + description: ippool list contains the global pool details. elements: dict type: list suboptions: dhcp_server_ips: - description: Dhcp Server Ips. + description: > + Responsible for automatically assigning IP addresses and + network configuration parameters to devices on a local network. elements: str type: list dns_server_ips: - description: Dns Server Ips. + description: Responsible for translating domain names into corresponding IP addresses. elements: str type: list gateway: - description: Gateway. + description: Serves as an entry or exit point for data traffic between networks. type: str ip_address_space: - description: Ip address space. + description: Ip address space either IPv4 or IPv6. type: str cidr: - description: Ip pool cidr. + description: Allows efficient allocation of address space. type: str prev_name: - description: previous name. + description: > + Previous name of the global pool. + Use this only for changing the name of the global pool. type: str name: - description: Ip Pool Name. + description: Name of the Global Ip Pool. type: str + type: + description: > + Includes both the Generic Ip Pool and Tunnel Ip Pool. + Generic - Used for general purpose within the network such as device + management or communication between the network devices. + Tunnel - Designated for the tunnel interfaces to encapsulate packetes + within the network protocol. It is used in VPN connections, + GRE tunnels, or other types of overlay networks. + default: Generic + choices: [Generic, Tunnel] + type: str + reserve_pool_details: description: Reserving IP subpool from the global pool type: dict suboptions: - ipv4DhcpServers: + ipv4_dhcp_servers: description: IPv4 input for dhcp server ip example 1.1.1.1. elements: str type: list @@ -89,7 +105,7 @@ description: IPv4 input for dns server ip example 4.4.4.4. elements: str type: list - ipv4GateWay: + ipv4_gateway: description: Gateway ip address details, example 175.175.0.1. type: str version_added: 4.0.0 @@ -105,7 +121,7 @@ ipv4_subnet: description: IPv4 Subnet address, example 175.175.0.0. type: str - ipv4TotalHost: + ipv4_total_host: description: IPv4 total host is required when ipv4_prefix value is false. type: int ipv6_address_space: @@ -113,15 +129,15 @@ If the value is false only ipv4 input are required, otherwise both ipv6 and ipv4 are required. type: bool - ipv6DhcpServers: + ipv6_dhcp_servers: description: IPv6 format dhcp server as input example 2001 db8 1234. elements: str type: list - ipv6DnsServers: + ipv6_dns_servers: description: IPv6 format dns server input example 2001 db8 1234. elements: str type: list - ipv6GateWay: + ipv6_gateway: description: Gateway ip address details, example 2001 db8 85a3 0 100 1. type: str ipv6_global_pool: @@ -140,7 +156,7 @@ ipv6_subnet: description: IPv6 Subnet address, example 2001 db8 85a3 0 100. type: str - ipv6TotalHost: + ipv6_total_host: description: IPv6 total host is required when ipv6_prefix value is false. type: int name: @@ -153,10 +169,24 @@ description: Site name path parameter. Site name to reserve the ip sub pool. type: str slaac_support: - description: Slaac Support. + description: > + Enables devices in IPv6 networks to automatically + configure their addresses without manual intervention. type: bool type: description: Type of the reserve ip sub pool. + Generic - Used for general purpose within the network such as device + management or communication between the network devices. + LAN - Used for the devices and the resources within the Local Area Network + such as device connectivity, internal communication, or services. + Management - Used for the management purposes such as device management interfaces, + management access, or other administrative functions. + Service - Used for the network services and application such as DNS (Domain Name System), + DHCP (Dynamic Host Configuration Protocol), NTP (Network Time Protocol). + WAN - Used for the devices and resources with the Wide Area Network such as remote + sites interconnection with other network or services hosted within WAN. + default: Generic + choices: [Generic, LAN, Management, Service, WAN] type: str network_management_details: description: Set default network settings for the site @@ -763,7 +793,7 @@ def get_reserve_pool_params(self, pool_info): reserve_pool.update({"ipv6GateWay": pool_info.get("ipPools")[1].get("gateways")[0]}) else: - reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"ipv6GateWay": ""}) elif not pool_info.get("ipPools")[1].get("ipv6"): reserve_pool.update({ @@ -783,7 +813,7 @@ def get_reserve_pool_params(self, pool_info): reserve_pool.update({"ipv6GateWay": pool_info.get("ipPools")[0].get("gateways")[0]}) else: - reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"ipv6GateWay": ""}) reserve_pool.update({"slaacSupport": True}) self.log("Formatted reserve pool details: {0}".format(reserve_pool), "DEBUG") return reserve_pool @@ -1305,19 +1335,19 @@ def get_want_reserve_pool(self, reserve_pool): "ipv4GlobalPool": reserve_pool.get("ipv4_global_pool"), "ipv4Prefix": reserve_pool.get("ipv4_prefix"), "ipv4PrefixLength": reserve_pool.get("ipv4_prefix_length"), - "ipv4GateWay": reserve_pool.get("ipv4GateWay"), - "ipv4DhcpServers": reserve_pool.get("ipv4DhcpServers"), + "ipv4GateWay": reserve_pool.get("ipv4_gateway"), + "ipv4DhcpServers": reserve_pool.get("ipv4_dhcp_servers"), "ipv4DnsServers": reserve_pool.get("ipv4_dns_servers"), "ipv4Subnet": reserve_pool.get("ipv4_subnet"), "ipv6GlobalPool": reserve_pool.get("ipv6_global_pool"), "ipv6Prefix": reserve_pool.get("ipv6_prefix"), "ipv6PrefixLength": reserve_pool.get("ipv6_prefix_length"), - "ipv6GateWay": reserve_pool.get("ipv6GateWay"), - "ipv6DhcpServers": reserve_pool.get("ipv6DhcpServers"), + "ipv6GateWay": reserve_pool.get("ipv6_gateway"), + "ipv6DhcpServers": reserve_pool.get("ipv6_dhcp_servers"), "ipv6Subnet": reserve_pool.get("ipv6_subnet"), - "ipv6DnsServers": reserve_pool.get("ipv6DnsServers"), - "ipv4TotalHost": reserve_pool.get("ipv4TotalHost"), - "ipv6TotalHost": reserve_pool.get("ipv6TotalHost") + "ipv6DnsServers": reserve_pool.get("ipv6_dns_servers"), + "ipv4TotalHost": reserve_pool.get("ipv4_total_host"), + "ipv6TotalHost": reserve_pool.get("ipv6_total_host") } # Check for missing mandatory parameters in the playbook diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 5ef8354832..afad220b80 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -42,7 +42,7 @@ required: true suboptions: global_pool_details: - description: Global ip pool manages IPv4 and IPv6 IP pools. + description: Manages IPv4 and IPv6 IP pools in the global level. type: dict suboptions: settings: @@ -50,38 +50,54 @@ type: dict suboptions: ip_pool: - description: Global Pool's ippool. + description: ippool list contains the global pool details. elements: dict type: list suboptions: dhcp_server_ips: - description: Dhcp Server Ips. + description: > + Responsible for automatically assigning IP addresses and + network configuration parameters to devices on a local network. elements: str type: list dns_server_ips: - description: Dns Server Ips. + description: Responsible for translating domain names into corresponding IP addresses. elements: str type: list gateway: - description: Gateway. + description: Serves as an entry or exit point for data traffic between networks. type: str ip_address_space: - description: Ip address space. + description: Ip address space either IPv4 or IPv6. type: str cidr: - description: Ip pool cidr. + description: Allows efficient allocation of address space. type: str prev_name: - description: previous name. + description: > + Previous name of the global pool. + Use this only for changing the name of the global pool. type: str name: - description: Ip Pool Name. + description: Name of the Global Ip Pool. type: str + type: + description: > + Includes both the Generic Ip Pool and Tunnel Ip Pool. + Generic - Used for general purpose within the network such as device + management or communication between the network devices. + Tunnel - Designated for the tunnel interfaces to encapsulate packetes + within the network protocol. It is used in VPN connections, + GRE tunnels, or other types of overlay networks. + default: Generic + choices: [Generic, Tunnel] + type: str + reserve_pool_details: description: Reserving IP subpool from the global pool type: dict suboptions: - ipv4DhcpServers: + ipv4_dhcp_servers: description: IPv4 input for dhcp server ip example 1.1.1.1. elements: str type: list @@ -89,7 +105,7 @@ description: IPv4 input for dns server ip example 4.4.4.4. elements: str type: list - ipv4GateWay: + ipv4_gateway: description: Gateway ip address details, example 175.175.0.1. type: str version_added: 4.0.0 @@ -105,7 +121,7 @@ ipv4_subnet: description: IPv4 Subnet address, example 175.175.0.0. type: str - ipv4TotalHost: + ipv4_total_host: description: IPv4 total host is required when ipv4_prefix value is false. type: int ipv6_address_space: @@ -113,15 +129,15 @@ If the value is false only ipv4 input are required, otherwise both ipv6 and ipv4 are required. type: bool - ipv6DhcpServers: + ipv6_dhcp_servers: description: IPv6 format dhcp server as input example 2001 db8 1234. elements: str type: list - ipv6DnsServers: + ipv6_dns_servers: description: IPv6 format dns server input example 2001 db8 1234. elements: str type: list - ipv6GateWay: + ipv6_gateway: description: Gateway ip address details, example 2001 db8 85a3 0 100 1. type: str ipv6_global_pool: @@ -140,7 +156,7 @@ ipv6_subnet: description: IPv6 Subnet address, example 2001 db8 85a3 0 100. type: str - ipv6TotalHost: + ipv6_total_host: description: IPv6 total host is required when ipv6_prefix value is false. type: int name: @@ -153,10 +169,24 @@ description: Site name path parameter. Site name to reserve the ip sub pool. type: str slaac_support: - description: Slaac Support. + description: > + Enables devices in IPv6 networks to automatically + configure their addresses without manual intervention. type: bool type: description: Type of the reserve ip sub pool. + Generic - Used for general purpose within the network such as device + management or communication between the network devices. + LAN - Used for the devices and the resources within the Local Area Network + such as device connectivity, internal communication, or services. + Management - Used for the management purposes such as device management interfaces, + management access, or other administrative functions. + Service - Used for the network services and application such as DNS (Domain Name System), + DHCP (Dynamic Host Configuration Protocol), NTP (Network Time Protocol). + WAN - Used for the devices and resources with the Wide Area Network such as remote + sites interconnection with other network or services hosted within WAN. + default: Generic + choices: [Generic, LAN, Management, Service, WAN] type: str network_management_details: description: Set default network settings for the site @@ -762,7 +792,7 @@ def get_reserve_pool_params(self, pool_info): reserve_pool.update({"ipv6GateWay": pool_info.get("ipPools")[1].get("gateways")[0]}) else: - reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"ipv6GateWay": ""}) elif not pool_info.get("ipPools")[1].get("ipv6"): reserve_pool.update({ @@ -782,7 +812,7 @@ def get_reserve_pool_params(self, pool_info): reserve_pool.update({"ipv6GateWay": pool_info.get("ipPools")[0].get("gateways")[0]}) else: - reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"ipv6GateWay": ""}) reserve_pool.update({"slaacSupport": True}) self.log("Formatted reserve pool details: {0}".format(reserve_pool), "DEBUG") return reserve_pool @@ -1304,19 +1334,19 @@ def get_want_reserve_pool(self, reserve_pool): "ipv4GlobalPool": reserve_pool.get("ipv4_global_pool"), "ipv4Prefix": reserve_pool.get("ipv4_prefix"), "ipv4PrefixLength": reserve_pool.get("ipv4_prefix_length"), - "ipv4GateWay": reserve_pool.get("ipv4GateWay"), - "ipv4DhcpServers": reserve_pool.get("ipv4DhcpServers"), + "ipv4GateWay": reserve_pool.get("ipv4_gateway"), + "ipv4DhcpServers": reserve_pool.get("ipv4_dhcp_servers"), "ipv4DnsServers": reserve_pool.get("ipv4_dns_servers"), "ipv4Subnet": reserve_pool.get("ipv4_subnet"), "ipv6GlobalPool": reserve_pool.get("ipv6_global_pool"), "ipv6Prefix": reserve_pool.get("ipv6_prefix"), "ipv6PrefixLength": reserve_pool.get("ipv6_prefix_length"), - "ipv6GateWay": reserve_pool.get("ipv6GateWay"), - "ipv6DhcpServers": reserve_pool.get("ipv6DhcpServers"), + "ipv6GateWay": reserve_pool.get("ipv6_gateway"), + "ipv6DhcpServers": reserve_pool.get("ipv6_dhcp_servers"), "ipv6Subnet": reserve_pool.get("ipv6_subnet"), - "ipv6DnsServers": reserve_pool.get("ipv6DnsServers"), - "ipv4TotalHost": reserve_pool.get("ipv4TotalHost"), - "ipv6TotalHost": reserve_pool.get("ipv6TotalHost") + "ipv6DnsServers": reserve_pool.get("ipv6_dns_servers"), + "ipv4TotalHost": reserve_pool.get("ipv4_total_host"), + "ipv6TotalHost": reserve_pool.get("ipv6_total_host") } # Check for missing mandatory parameters in the playbook From 12b55c16515b03379e3d0e845f57049b30f36aa9 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 14 Feb 2024 20:05:29 +0530 Subject: [PATCH 33/64] add brief description for each paramter in both intent module and workflow manager module for Swim and Inventory and addresses PR review comments as well. --- plugins/modules/inventory_intent.py | 82 ++++++++++++------- plugins/modules/inventory_workflow_manager.py | 81 +++++++++++------- plugins/modules/swim_intent.py | 47 +++++------ plugins/modules/swim_workflow_manager.py | 43 +++++----- 4 files changed, 150 insertions(+), 103 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 85ad9b6d3f..3dfb38d79f 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -44,28 +44,32 @@ protocol (either SSH or Telnet) used by the device. type: str compute_device: - description: Compute Device flag. + description: Indicates whether a device is a compute device. type: bool + password: + description: Password for accessing the device and for file encryption during device export. Required for + adding Network Device. Also needed for file encryption while exporting device in a csv file. + type: str enable_password: - description: Device's enable password. + description: Password required for enabling configurations on the device. type: str extended_discovery_info: - description: Device's extended discovery info. + description: Additional discovery information for the device. type: str http_password: - description: Device's http password. Required for Adding Compute, Meraki, Firepower Management Devices. + description: HTTP password required for adding compute, Meraki, and Firepower Management Devices. type: str http_port: - description: Device's http port number. Required for Adding Compute, Firepower Management Devices. + description: HTTP port number required for adding compute and Firepower Management Devices. type: str http_secure: - description: HttpSecure flag. + description: Flag indicating HTTP security. type: bool http_username: - description: Device's http username. Required for Adding Compute,Firepower Management Devices. + description: HTTP username required for adding compute and Firepower Management Devices. type: str ip_address: - description: Device's ipAddress. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. + description: IP address of the device. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. elements: str type: list hostname_list: @@ -84,53 +88,58 @@ elements: str type: list netconf_port: - description: Device's netconf port. + description: Netconf port number. type: str username: - description: Network Device's username. Required for Adding Network Device. - type: str - password: - description: Device's password. Required for Adding Network Device. - Also needed for file encryption while exporting device in a csv file. + description: Username for accessing the device. Required for Adding Network Device. type: str serial_number: - description: Device's serial number. + description: Serial number of the device. type: str snmp_auth_passphrase: - description: Device's snmp auth passphrase. Required for Adding Network, Compute, Third Party Devices. + description: SNMP authentication passphrase required for adding network, compute, and third-party devices. type: str snmp_auth_protocol: - description: Device's snmp Auth Protocol. + description: SNMP authentication protocol. + SHA (Secure Hash Algorithm) - cryptographic hash function commonly used for data integrity verification and authentication purposes. + MD5 (Message Digest Algorithm 5) - cryptographic hash function commonly used for data integrity verification and authentication purposes. type: str default: "SHA" snmp_mode: - description: Device's snmp Mode. + description: Device's snmp Mode refer to different SNMP (Simple Network Management Protocol) versions and their corresponding security levels. + NOAUTHNOPRIV - This mode provides no authentication or encryption for SNMP messages. It means that devices communicating using SNMPv1 do + not require any authentication (username/password) or encryption (data confidentiality). This makes it the least secure option. + AUTHNOPRIV - This mode provides authentication but no encryption for SNMP messages. Authentication involves validating the source of the + SNMP messages using a community string (similar to a password). However, the data transmitted between devices is not encrypted, + so it's susceptible to eavesdropping. + AUTHPRIV - This mode provides both authentication and encryption for SNMP messages. It offers the highest level of security among the three + options. Authentication ensures that the source of the messages is genuine, and encryption ensures that the data exchanged between + devices is confidential and cannot be intercepted by unauthorized parties. type: str snmp_priv_passphrase: - description: Device's snmp Private Passphrase. Required for Adding Network, Compute, Third Party Devices. + description: SNMP private passphrase required for adding network, compute, and third-party devices. type: str snmp_priv_protocol: - description: Device's snmp Private Protocol. Required for Adding Network, Compute, Third Party Devices. - Must be given in playbook if you are updating the device credentails. + description: SNMP private protocol required for adding network, compute, and third-party devices. type: str snmp_ro_community: - description: Device's snmp ROCommunity. Required for Adding V2C Devices. + description: SNMP Read-Only community required for adding V2C devices. type: str default: public snmp_rw_community: - description: Device's snmp RWCommunity. Required for Adding V2C Devices. + description: SNMP Read-Write community required for adding V2C devices. type: str default: private snmp_retry: - description: Device's snmp Retry. + description: SNMP retry count. type: int default: 3 snmp_timeout: - description: Device's snmp Timeout. + description: SNMP timeout duration. type: int default: 5 snmp_username: - description: Device's snmp Username. Required for Adding Network, Compute, Third Party Devices. + description: SNMP username required for adding network, compute, and third-party devices. type: str snmp_version: description: Device's snmp Version. @@ -138,10 +147,22 @@ default: "v3" type: description: Select Device's type from NETWORK_DEVICE, COMPUTE_DEVICE, MERAKI_DASHBOARD, THIRD_PARTY_DEVICE, FIREPOWER_MANAGEMENT_SYSTEM. + NETWORK_DEVICE - This refers to traditional networking equipment such as routers, switches, access points, and firewalls. These devices + are responsible for routing, switching, and providing connectivity within the network. + COMPUTE_DEVICE - These are computing resources such as servers, virtual machines, or containers that are part of the network infrastructure. + Cisco DNA Center can integrate with compute devices to provide visibility and management capabilities, ensuring that the network and + compute resources work together seamlessly to support applications and services. + MERAKI_DASHBOARD - It is cloud-based platform used to manage Meraki networking devices, including wireless access points, switches, security + appliances, and cameras. + THIRD_PARTY_DEVICE - This category encompasses devices from vendors other than Cisco or Meraki. Cisco DNA Center is designed to support + integration with third-party devices through open standards and APIs. This allows organizations to manage heterogeneous network + environments efficiently using Cisco DNA Center's centralized management and automation capabilities. + FIREPOWER_MANAGEMENT_SYSTEM - It is a centralized management console used to manage Cisco's Firepower Next-Generation Firewall (NGFW) devices. + It provides features such as policy management, threat detection, and advanced security analytics. type: str default: "NETWORK_DEVICE" update_mgmt_ipaddresslist: - description: Network Device's update Mgmt IPaddress List. + description: List of updated management IP addresses for network devices. type: list elements: dict suboptions: @@ -197,7 +218,7 @@ type: str default: "ACCESS" role_source: - description: role source for the Device. + description: Role source for the device. type: str default: "AUTO" name: @@ -229,6 +250,8 @@ type: str operation_enum: description: enum(CREDENTIALDETAILS, DEVICEDETAILS) 0 to export Device Credential Details Or 1 to export Device Details. + CREDENTIALDETAILS - Used for exporting device credentials details like snpm credntials, device crdentails etc. + DEVICEDETAILS - Used for exporting device specific details like device hostname, serial number, type, family etc. type: str parameters: description: List of device parameters that needs to be exported to file. @@ -1768,8 +1791,7 @@ def get_site_type(self, site_name): site_type = item.get("attributes").get("type") except Exception as e: - self.msg = "Error while fetching the site '{0}' and given site not found in Cisco Catalyst Center".format(site_name) - self.log(self.msg, "ERROR") + self.msg = "Error while fetching the site '{0}' and the specified site was not found in Cisco Catalyst Center.".format(site_name) self.module.fail_json(msg=self.msg, response=[self.msg]) return site_type diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index ac822b9cd8..b10d6dac8e 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -44,28 +44,32 @@ protocol (either SSH or Telnet) used by the device. type: str compute_device: - description: Compute Device flag. + description: Indicates whether a device is a compute device. type: bool + password: + description: Password for accessing the device and for file encryption during device export. Required for + adding Network Device. Also needed for file encryption while exporting device in a csv file. + type: str enable_password: - description: Device's enable password. + description: Password required for enabling configurations on the device. type: str extended_discovery_info: - description: Device's extended discovery info. + description: Additional discovery information for the device. type: str http_password: - description: Device's http password. Required for Adding Compute, Meraki, Firepower Management Devices. + description: HTTP password required for adding compute, Meraki, and Firepower Management Devices. type: str http_port: - description: Device's http port number. Required for Adding Compute, Firepower Management Devices. + description: HTTP port number required for adding compute and Firepower Management Devices. type: str http_secure: - description: HttpSecure flag. + description: Flag indicating HTTP security. type: bool http_username: - description: Device's http username. Required for Adding Compute,Firepower Management Devices. + description: HTTP username required for adding compute and Firepower Management Devices. type: str ip_address: - description: Device's ipAddress. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. + description: IP address of the device. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. elements: str type: list hostname_list: @@ -84,53 +88,58 @@ elements: str type: list netconf_port: - description: Device's netconf port. + description: Netconf port number. type: str username: - description: Network Device's username. Required for Adding Network Device. - type: str - password: - description: Device's password. Required for Adding Network Device. - Also needed for file encryption while exporting device in a csv file. + description: Username for accessing the device. Required for Adding Network Device. type: str serial_number: - description: Device's serial number. + description: Serial number of the device. type: str snmp_auth_passphrase: - description: Device's snmp auth passphrase. Required for Adding Network, Compute, Third Party Devices. + description: SNMP authentication passphrase required for adding network, compute, and third-party devices. type: str snmp_auth_protocol: - description: Device's snmp Auth Protocol. + description: SNMP authentication protocol. + SHA (Secure Hash Algorithm) - cryptographic hash function commonly used for data integrity verification and authentication purposes. + MD5 (Message Digest Algorithm 5) - cryptographic hash function commonly used for data integrity verification and authentication purposes. type: str default: "SHA" snmp_mode: - description: Device's snmp Mode. + description: Device's snmp Mode refer to different SNMP (Simple Network Management Protocol) versions and their corresponding security levels. + NOAUTHNOPRIV - This mode provides no authentication or encryption for SNMP messages. It means that devices communicating using SNMPv1 do + not require any authentication (username/password) or encryption (data confidentiality). This makes it the least secure option. + AUTHNOPRIV - This mode provides authentication but no encryption for SNMP messages. Authentication involves validating the source of the + SNMP messages using a community string (similar to a password). However, the data transmitted between devices is not encrypted, + so it's susceptible to eavesdropping. + AUTHPRIV - This mode provides both authentication and encryption for SNMP messages. It offers the highest level of security among the three + options. Authentication ensures that the source of the messages is genuine, and encryption ensures that the data exchanged between + devices is confidential and cannot be intercepted by unauthorized parties. type: str snmp_priv_passphrase: - description: Device's snmp Private Passphrase. Required for Adding Network, Compute, Third Party Devices. + description: SNMP private passphrase required for adding network, compute, and third-party devices. type: str snmp_priv_protocol: - description: Device's snmp Private Protocol. Required for Adding Network, Compute, Third Party Devices. - Must be given in playbook if you are updating the device credentails. + description: SNMP private protocol required for adding network, compute, and third-party devices. type: str snmp_ro_community: - description: Device's snmp ROCommunity. Required for Adding V2C Devices. + description: SNMP Read-Only community required for adding V2C devices. type: str default: public snmp_rw_community: - description: Device's snmp RWCommunity. Required for Adding V2C Devices. + description: SNMP Read-Write community required for adding V2C devices. type: str default: private snmp_retry: - description: Device's snmp Retry. + description: SNMP retry count. type: int default: 3 snmp_timeout: - description: Device's snmp Timeout. + description: SNMP timeout duration. type: int default: 5 snmp_username: - description: Device's snmp Username. Required for Adding Network, Compute, Third Party Devices. + description: SNMP username required for adding network, compute, and third-party devices. type: str snmp_version: description: Device's snmp Version. @@ -138,10 +147,22 @@ default: "v3" type: description: Select Device's type from NETWORK_DEVICE, COMPUTE_DEVICE, MERAKI_DASHBOARD, THIRD_PARTY_DEVICE, FIREPOWER_MANAGEMENT_SYSTEM. + NETWORK_DEVICE - This refers to traditional networking equipment such as routers, switches, access points, and firewalls. These devices + are responsible for routing, switching, and providing connectivity within the network. + COMPUTE_DEVICE - These are computing resources such as servers, virtual machines, or containers that are part of the network infrastructure. + Cisco DNA Center can integrate with compute devices to provide visibility and management capabilities, ensuring that the network and + compute resources work together seamlessly to support applications and services. + MERAKI_DASHBOARD - It is cloud-based platform used to manage Meraki networking devices, including wireless access points, switches, security + appliances, and cameras. + THIRD_PARTY_DEVICE - This category encompasses devices from vendors other than Cisco or Meraki. Cisco DNA Center is designed to support + integration with third-party devices through open standards and APIs. This allows organizations to manage heterogeneous network + environments efficiently using Cisco DNA Center's centralized management and automation capabilities. + FIREPOWER_MANAGEMENT_SYSTEM - It is a centralized management console used to manage Cisco's Firepower Next-Generation Firewall (NGFW) devices. + It provides features such as policy management, threat detection, and advanced security analytics. type: str default: "NETWORK_DEVICE" update_mgmt_ipaddresslist: - description: Network Device's update Mgmt IPaddress List. + description: List of updated management IP addresses for network devices. type: list elements: dict suboptions: @@ -197,7 +218,7 @@ type: str default: "ACCESS" role_source: - description: role source for the Device. + description: Role source for the device. type: str default: "AUTO" name: @@ -229,6 +250,8 @@ type: str operation_enum: description: enum(CREDENTIALDETAILS, DEVICEDETAILS) 0 to export Device Credential Details Or 1 to export Device Details. + CREDENTIALDETAILS - Used for exporting device credentials details like snpm credntials, device crdentails etc. + DEVICEDETAILS - Used for exporting device specific details like device hostname, serial number, type, family etc. type: str parameters: description: List of device parameters that needs to be exported to file. @@ -1768,7 +1791,7 @@ def get_site_type(self, site_name): site_type = item.get("attributes").get("type") except Exception as e: - self.msg = "Error while fetching the site '{0}' and given site not found in Cisco Catalyst Center".format(site_name) + self.msg = "Error while fetching the site '{0}' and the specified site was not found in Cisco Catalyst Center.".format(site_name) self.log(self.msg, "ERROR") self.module.fail_json(msg=self.msg, response=[self.msg]) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 80f279fcd7..5fcc3c3c83 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -50,26 +50,26 @@ type: dict suboptions: type: - description: The source of import, supports remote import or local import. + description: Specifies the import source, supporting local file import (local) or remote url import (remote). type: str local_image_details: description: Details of the local path of the image to be imported. type: dict suboptions: file_path: - description: Give the file absolute path required while importing image from local. + description: Provide the absolute file path needed to import an image from your local system (Eg "/path/to/your/file"). type: str is_third_party: - description: IsThirdParty query parameter. Third party Image check (Optional). + description: Query parameter to determine if the image is from a third party (optional). type: bool third_party_application_type: - description: ThirdPartyApplicationType query parameter. Third Party Application Type (Optional). + description: Specify the ThirdPartyApplicationType query parameter to indicate the type of third-party application.(optional) type: str third_party_image_family: - description: ThirdPartyImageFamily query parameter. Third Party image family (Optional). + description: Provide the ThirdPartyImageFamily query parameter to identify the family of the third-party image. (optional) type: str third_party_vendor: - description: ThirdPartyVendor query parameter (Optional). + description: Include the ThirdPartyVendor query parameter to specify the vendor of the third party. type: str url_details: description: URL details for SWIM import @@ -81,20 +81,21 @@ elements: dict suboptions: application_type: - description: Optional parameter indicating the type of application with permitted values(WLC, LINUX, FILREWALL, WINDOWS, - LOADBALANCER, THIRDPARTY etc) applicable only for third party image types. + description: An optional parameter that specifies the type of application. Allowed values include WLC, LINUX, FIREWALL, WINDOWS, + LOADBALANCER, THIRDPARTY, etc. This is only applicable for third-party image types. type: str image_family: - description: The name of image family applicable only in case of third party images upload (Optional). + description: Represents the name of the image family and is applicable only when uploading third-party images (Optional). type: str source_url: - description: Required parameter for importing swim image via remote url. + description: A mandatory parameter for importing a SWIM image via a remote URL. This parameter is required when using a URL + to import an image. type: str is_third_party: - description: Set the boolean value if image is uploaded from third party (Optional). + description: Flag indicates whether the image is uploaded from a third party (optional). type: bool vendor: - description: Name of vendor applicable only for third party image types (Optional). + description: The name of the vendor, that applies only to third-party image types when importing via URL (Optional). type: str schedule_at: description: ScheduleAt query parameter. Epoch Time (The number of milli-seconds since @@ -114,8 +115,8 @@ description: SWIM image name which will be tagged or untagged as golden. type: str device_role: - description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, - DISTRIBUTION and CORE. + description: Defines the device role, with permissible values including ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION, and CORE. ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. This could happen if the platform is unable to determine the device's role based on available information. @@ -149,8 +150,8 @@ type: dict suboptions: device_role: - description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, - DISTRIBUTION and CORE. + description: Defines the device role, with permissible values including ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION, and CORE. ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. This could happen if the platform is unable to determine the device's role based on available information. @@ -166,7 +167,7 @@ providing interconnection between different network segments. type: str device_family_name: - description: Name of the device family(Switches and Hubs etc.) + description: Specify the name of the device family such as Switches and Hubs, etc. type: str site_name: description: Used to get device details associated to this site. @@ -193,8 +194,8 @@ type: dict suboptions: device_role: - description: Device Role and permissible Values are ALL, UNKNOWN, ACCESS, BORDER ROUTER, - DISTRIBUTION and CORE. + description: Defines the device role, with permissible values including ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION, and CORE. ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. This could happen if the platform is unable to determine the device's role based on available information. @@ -210,7 +211,7 @@ providing interconnection between different network segments. type: str device_family_name: - description: Name of the device family(Switches and Hubs etc.) + description: Specify the name of the device family such as Switches and Hubs, etc. type: str site_name: description: Used to get device details associated to this site. @@ -224,15 +225,15 @@ When this mode is selected, the existing image on the device is completely replaced with the new image during the upgrade process. This ensures that the device runs only the new image version after the upgrade is completed. bundle - This mode instructs Cisco Catalyst Center bundles the new image with the existing image on the device before initiating - the upgrade process.This mode allows for a more efficient upgrade process by preserving the existing image on the device while - adding the new image as an additional bundle.After the upgrade, the device can run either the existing image or the new bundled + the upgrade process. This mode allows for a more efficient upgrade process by preserving the existing image on the device while + adding the new image as an additional bundle. After the upgrade, the device can run either the existing image or the new bundled image, depending on the configuration. currentlyExists - This mode instructs Cisco Catalyst Center to checks if the target devices already have the desired image version installed. If image already present on devices, no action is taken and upgrade process is skipped for those devices. This mode is useful for avoiding unnecessary upgrades on devices that already have the correct image version installed, thereby saving time. type: str distribute_if_needed: - description: Set the distribute_if_needed flag while activating the swim image. + description: Enable the distribute_if_needed option when activating the SWIM image. type: bool image_name: description: SWIM image's name diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index a2a26686ae..ada17cfdf2 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -50,26 +50,26 @@ type: dict suboptions: type: - description: The source of import, supports remote import or local import. + description: Specifies the import source, supporting local file import (local) or remote url import (remote). type: str local_image_details: description: Details of the local path of the image to be imported. type: dict suboptions: file_path: - description: Give the file absolute path required while importing image from local. + description: Provide the absolute file path needed to import an image from your local system (Eg "/path/to/your/file"). type: str is_third_party: - description: IsThirdParty query parameter. Third party Image check (Optional). + description: Query parameter to determine if the image is from a third party (optional). type: bool third_party_application_type: - description: ThirdPartyApplicationType query parameter. Third Party Application Type (Optional). + description: Specify the ThirdPartyApplicationType query parameter to indicate the type of third-party application.(optional) type: str third_party_image_family: - description: ThirdPartyImageFamily query parameter. Third Party image family (Optional). + description: Provide the ThirdPartyImageFamily query parameter to identify the family of the third-party image. (optional) type: str third_party_vendor: - description: ThirdPartyVendor query parameter (Optional). + description: Include the ThirdPartyVendor query parameter to specify the vendor of the third party. type: str url_details: description: URL details for SWIM import @@ -81,20 +81,21 @@ elements: dict suboptions: application_type: - description: Optional parameter indicating the type of application with permitted values(WLC, LINUX, FILREWALL, WINDOWS, - LOADBALANCER, THIRDPARTY etc) applicable only for third party image types. + description: An optional parameter that specifies the type of application. Allowed values include WLC, LINUX, FIREWALL, WINDOWS, + LOADBALANCER, THIRDPARTY, etc. This is only applicable for third-party image types. type: str image_family: - description: The name of image family applicable only in case of third party images upload (Optional). + description: Represents the name of the image family and is applicable only when uploading third-party images (Optional). type: str source_url: - description: Required parameter for importing swim image via remote url. + description: A mandatory parameter for importing a SWIM image via a remote URL. This parameter is required when using a URL + to import an image. type: str is_third_party: - description: Set the boolean value if image is uploaded from third party (Optional). + description: Flag indicates whether the image is uploaded from a third party (optional). type: bool vendor: - description: Name of vendor applicable only for third party image types (Optional). + description: The name of the vendor, that applies only to third-party image types when importing via URL (Optional). type: str schedule_at: description: ScheduleAt query parameter. Epoch Time (The number of milli-seconds since @@ -114,8 +115,8 @@ description: SWIM image name which will be tagged or untagged as golden. type: str device_role: - description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, - DISTRIBUTION and CORE. + description: Defines the device role, with permissible values including ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION, and CORE. ALL - This role typically represents all devices within the network, regardless of their specific roles or functions. UNKNOWN - This role is assigned to devices whose roles or functions have not been identified or classified within Cisco Catalsyt Center. This could happen if the platform is unable to determine the device's role based on available information. @@ -166,7 +167,7 @@ providing interconnection between different network segments. type: str device_family_name: - description: Name of the device family(Switches and Hubs etc.) + description: Specify the name of the device family such as Switches and Hubs, etc. type: str site_name: description: Used to get device details associated to this site. @@ -193,11 +194,11 @@ type: dict suboptions: device_role: - description: Device Role and permissible Values are ALL, UNKNOWN, ACCESS, BORDER ROUTER, - DISTRIBUTION and CORE. + description: Defines the device role, with permissible values including ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION, and CORE. type: str device_family_name: - description: Name of the device family(Switches and Hubs etc.) + description: Specify the name of the device family such as Switches and Hubs, etc. type: str site_name: description: Used to get device details associated to this site. @@ -211,15 +212,15 @@ When this mode is selected, the existing image on the device is completely replaced with the new image during the upgrade process. This ensures that the device runs only the new image version after the upgrade is completed. bundle - This mode instructs Cisco Catalyst Center bundles the new image with the existing image on the device before initiating - the upgrade process.This mode allows for a more efficient upgrade process by preserving the existing image on the device while - adding the new image as an additional bundle.After the upgrade, the device can run either the existing image or the new bundled + the upgrade process. This mode allows for a more efficient upgrade process by preserving the existing image on the device while + adding the new image as an additional bundle. After the upgrade, the device can run either the existing image or the new bundled image, depending on the configuration. currentlyExists - This mode instructs Cisco Catalyst Center to checks if the target devices already have the desired image version installed. If image already present on devices, no action is taken and upgrade process is skipped for those devices. This mode is useful for avoiding unnecessary upgrades on devices that already have the correct image version installed, thereby saving time. type: str distribute_if_needed: - description: Set the distribute_if_needed flag while activating the swim image. + description: Enable the distribute_if_needed option when activating the SWIM image. type: bool image_name: description: SWIM image's name From c10eaa63550739bb825cce5f7278d26ae005bc17 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 15 Feb 2024 15:02:07 +0530 Subject: [PATCH 34/64] Addressed the PR comments --- playbooks/network_settings_intent.yml | 3 +- .../network_settings_workflow_manager.yml | 5 +- plugins/modules/network_settings_intent.py | 106 ++++++++++++------ .../network_settings_workflow_manager.py | 102 ++++++++++------- plugins/modules/template_intent.py | 6 +- plugins/modules/template_workflow_manager.py | 6 +- 6 files changed, 140 insertions(+), 88 deletions(-) diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index da4f2dac2d..92d045a4d2 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -30,7 +30,7 @@ gateway: '' #use this for updating ip_address_space: IPv6 #required when we are creating cidr: 2001:db8::/64 #required when we are creating - type: Generic + pool_type: Generic dhcp_server_ips: [] #use this for updating dns_server_ips: [] #use this for updating # prev_name: Global_Pool2 @@ -40,6 +40,7 @@ ipv4_prefix: True ipv4_prefix_length: 9 ipv4_subnet: 100.128.0.0 + ipv4_gateway: 100.128.0.1 # ipv4_dns_servers: [100.128.0.1] name: IP_Pool_3 ipv6_prefix: True diff --git a/playbooks/network_settings_workflow_manager.yml b/playbooks/network_settings_workflow_manager.yml index 84d965cb14..36b88ac2db 100644 --- a/playbooks/network_settings_workflow_manager.yml +++ b/playbooks/network_settings_workflow_manager.yml @@ -26,7 +26,7 @@ gateway: '' #use this for updating ip_address_space: IPv6 #required when we are creating cidr: 2001:db8::/64 #required when we are creating - type: Generic + pool_type: Generic dhcp_server_ips: [] #use this for updating dns_server_ips: [] #use this for updating # prev_name: Global_Pool2 @@ -36,6 +36,7 @@ ipv4_prefix: True ipv4_prefix_length: 9 ipv4_subnet: 100.128.0.0 + ipv4_gateway: 100.128.0.1 # ipv4_dns_servers: [100.128.0.1] name: IP_Pool_3 ipv6_prefix: True @@ -45,7 +46,7 @@ site_name: Global/Chennai/Trill slaac_support: True # prev_name: IP_Pool_4 - type: LAN + pool_type: LAN network_management_details: settings: dhcp_server: diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index a0fe75e9b5..549cd05935 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -50,14 +50,14 @@ type: dict suboptions: ip_pool: - description: ippool list contains the global pool details. + description: Contains a list of global IP pool configurations. elements: dict type: list suboptions: dhcp_server_ips: description: > - Responsible for automatically assigning IP addresses and - network configuration parameters to devices on a local network. + The DHCP server IPs responsible for automatically assigning IP addresses + and network configuration parameters to devices on a local network. elements: str type: list dns_server_ips: @@ -68,25 +68,27 @@ description: Serves as an entry or exit point for data traffic between networks. type: str ip_address_space: - description: Ip address space either IPv4 or IPv6. + description: IP address space either IPv4 or IPv6. type: str cidr: - description: Allows efficient allocation of address space. + description: > + Defines the IP pool's Classless Inter-Domain Routing block, + enabling systematic IP address distribution within a network. type: str prev_name: description: > - Previous name of the global pool. - Use this only for changing the name of the global pool. + The former identifier for the global pool. It should be used + exclusively when you need to update the global pool's name. type: str name: - description: Name of the Global Ip Pool. + description: Specifies the name assigned to the Global IP Pool. type: str - type: + pool_type: description: > Includes both the Generic Ip Pool and Tunnel Ip Pool. Generic - Used for general purpose within the network such as device management or communication between the network devices. - Tunnel - Designated for the tunnel interfaces to encapsulate packetes + Tunnel - Designated for the tunnel interfaces to encapsulate packets within the network protocol. It is used in VPN connections, GRE tunnels, or other types of overlay networks. default: Generic @@ -94,19 +96,19 @@ type: str reserve_pool_details: - description: Reserving IP subpool from the global pool + description: Reserved IP subpool details from the global pool. type: dict suboptions: ipv4_dhcp_servers: - description: IPv4 input for dhcp server ip example 1.1.1.1. + description: Specifies the IPv4 addresses for DHCP servers, for example, "1.1.1.1". elements: str type: list ipv4_dns_servers: - description: IPv4 input for dns server ip example 4.4.4.4. + description: Specifies the IPv4 addresses for DNS servers, for example, "4.4.4.4". elements: str type: list ipv4_gateway: - description: Gateway ip address details, example 175.175.0.1. + description: Provides the gateway's IPv4 address, for example, "175.175.0.1". type: str version_added: 4.0.0 ipv4_global_pool: @@ -119,26 +121,33 @@ description: The ipv4 prefix length is required when ipv4_prefix value is true. type: int ipv4_subnet: - description: IPv4 Subnet address, example 175.175.0.0. + description: Indicates the IPv4 subnet address, for example, "175.175.0.0". type: str ipv4_total_host: - description: IPv4 total host is required when ipv4_prefix value is false. + description: The total number of hosts for IPv4, required when the 'ipv4_prefix' is set to false. type: int ipv6_address_space: description: > - If the value is false only ipv4 input are required, otherwise both - ipv6 and ipv4 are required. + Determines whether both IPv6 and IPv4 inputs are required. + If set to false, only IPv4 inputs are required. + If set to true, both IPv6 and IPv4 inputs are required. type: bool ipv6_dhcp_servers: - description: IPv6 format dhcp server as input example 2001 db8 1234. + description: > + Specifies the IPv6 addresses for DHCP servers in the format. + For example, "2001:0db8:0123:4567:89ab:cdef:0001:0001". elements: str type: list ipv6_dns_servers: - description: IPv6 format dns server input example 2001 db8 1234. + description: > + Specifies the IPv6 addresses for DNS servers. + For example, "2001:0db8:0123:4567:89ab:cdef:0002:0002". elements: str type: list ipv6_gateway: - description: Gateway ip address details, example 2001 db8 85a3 0 100 1. + description: > + Provides the gateway's IPv6 address. + For example, "2001:0db8:0123:4567:89ab:cdef:0003:0003". type: str ipv6_global_pool: description: > @@ -157,23 +166,25 @@ description: IPv6 Subnet address, example 2001 db8 85a3 0 100. type: str ipv6_total_host: - description: IPv6 total host is required when ipv6_prefix value is false. + description: The total number of hosts for IPv6 is required if the 'ipv6_prefix' is set to false. type: int name: - description: Name of the reserve ip sub pool. + description: Name of the reserve IP subpool. type: str prev_name: - description: Previous name of the reserve ip sub pool. + description: The former name associated with the reserved IP sub-pool. type: str site_name: - description: Site name path parameter. Site name to reserve the ip sub pool. + description: > + The name of the site provided as a path parameter, used + to specify where the IP sub-pool will be reserved. type: str slaac_support: description: > - Enables devices in IPv6 networks to automatically - configure their addresses without manual intervention. + Allows devices on IPv6 networks to self-configure their + IP addresses autonomously, eliminating the need for manual setup. type: bool - type: + pool_type: description: Type of the reserve ip sub pool. Generic - Used for general purpose within the network such as device management or communication between the network devices. @@ -279,7 +290,7 @@ description: Network V2's snmpServer. suboptions: configure_dnac_ip: - description: Configuration Cisco DNA Center IP for SNMP Server (eg true). + description: Configuration Cisco Catalyst Center IP for SNMP Server (eg true). type: bool ip_addresses: description: IP Address for SNMP Server (eg 4.4.4.1). @@ -290,7 +301,7 @@ description: Network V2's syslogServer. suboptions: configure_dnac_ip: - description: Configuration Cisco DNA Center IP for syslog server (eg true). + description: Configuration Cisco Catalyst Center IP for syslog server (eg true). type: bool ip_addresses: description: IP Address for syslog server (eg 4.4.4.4). @@ -301,7 +312,9 @@ description: Input for time zone (eg Africa/Abidjan). type: str site_name: - description: Site name path parameter. + description: > + The name of the site provided as a path parameter, used + to specify where the IP sub-pool will be reserved. type: str requirements: - dnacentersdk == 2.4.5 @@ -349,7 +362,7 @@ gateway: string ip_address_space: string cidr: string - type: Generic + pool_type: Generic dhcp_server_ips: list dns_server_ips: list reserve_pool_details: @@ -365,7 +378,7 @@ ipv6_subnet: string site_name: string slaac_support: True - type: LAN + pool_type: LAN network_management_details: settings: dhcp_server: list @@ -493,6 +506,10 @@ def validate_input(self): "cidr": {"type": 'string'}, "name": {"type": 'string'}, "prevName": {"type": 'string'}, + "pool_type": { + "type": 'string', + "choices": ["Generic", "LAN", "Management", "Service", "WAN"] + }, } } }, @@ -519,6 +536,10 @@ def validate_input(self): "ipv6TotalHost": {"type": 'integer'}, "slaac_support": {"type": 'bool'}, "site_name": {"type": 'string'}, + "pool_type": { + "type": 'string', + "choices": ["Generic", "LAN", "Management", "Service", "WAN"] + }, }, "network_management_details": { "type": 'dict', @@ -1273,7 +1294,7 @@ def get_want_global_pool(self, global_ippool): "ipPoolName": global_ippool.get("name"), "ipPoolCidr": global_ippool.get("cidr"), "gateway": global_ippool.get("gateway"), - "type": global_ippool.get("type"), + "type": global_ippool.get("pool_type"), }] } } @@ -1290,7 +1311,13 @@ def get_want_global_pool(self, global_ippool): if want_ippool.get("gateway") is None: want_ippool.update({"gateway": ""}) if want_ippool.get("type") is None: - want_ippool.update({"type": "Generic"}) + global_ippool_type = global_ippool.get("type") + if not global_ippool_type: + want_ippool.update({"type": "Generic"}) + else: + want_ippool.update({"type": global_ippool_type}) + self.log("'type' is deprecated and use 'pool_type'", "WARNING") + else: have_ippool = self.have.get("globalPool").get("details") \ .get("settings").get("ippool")[0] @@ -1330,7 +1357,7 @@ def get_want_reserve_pool(self, reserve_pool): want_reserve = { "name": reserve_pool.get("name"), - "type": reserve_pool.get("type"), + "type": reserve_pool.get("pool_type"), "ipv6AddressSpace": reserve_pool.get("ipv6_address_space"), "ipv4GlobalPool": reserve_pool.get("ipv4_global_pool"), "ipv4Prefix": reserve_pool.get("ipv4_prefix"), @@ -1387,7 +1414,12 @@ def get_want_reserve_pool(self, reserve_pool): return self if want_reserve.get("type") is None: - want_reserve.update({"type": "Generic"}) + reserve_pool_type = reserve_pool.get("type") + if not reserve_pool_type: + want_reserve.update({"type": "Generic"}) + else: + want_reserve.update({"type": reserve_pool_type}) + self.log("'type' is deprecated and use 'pool_type'", "WARNING") if want_reserve.get("ipv4GateWay") is None: want_reserve.update({"ipv4GateWay": ""}) if want_reserve.get("ipv4DhcpServers") is None: diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index afad220b80..952e8e931a 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -50,14 +50,14 @@ type: dict suboptions: ip_pool: - description: ippool list contains the global pool details. + description: Contains a list of global IP pool configurations. elements: dict type: list suboptions: dhcp_server_ips: description: > - Responsible for automatically assigning IP addresses and - network configuration parameters to devices on a local network. + The DHCP server IPs responsible for automatically assigning IP addresses + and network configuration parameters to devices on a local network. elements: str type: list dns_server_ips: @@ -68,25 +68,27 @@ description: Serves as an entry or exit point for data traffic between networks. type: str ip_address_space: - description: Ip address space either IPv4 or IPv6. + description: IP address space either IPv4 or IPv6. type: str cidr: - description: Allows efficient allocation of address space. + description: > + Defines the IP pool's Classless Inter-Domain Routing block, + enabling systematic IP address distribution within a network. type: str prev_name: description: > - Previous name of the global pool. - Use this only for changing the name of the global pool. + The former identifier for the global pool. It should be used + exclusively when you need to update the global pool's name. type: str name: - description: Name of the Global Ip Pool. + description: Specifies the name assigned to the Global IP Pool. type: str - type: + pool_type: description: > Includes both the Generic Ip Pool and Tunnel Ip Pool. Generic - Used for general purpose within the network such as device management or communication between the network devices. - Tunnel - Designated for the tunnel interfaces to encapsulate packetes + Tunnel - Designated for the tunnel interfaces to encapsulate packets within the network protocol. It is used in VPN connections, GRE tunnels, or other types of overlay networks. default: Generic @@ -94,19 +96,19 @@ type: str reserve_pool_details: - description: Reserving IP subpool from the global pool + description: Reserved IP subpool details from the global pool. type: dict suboptions: ipv4_dhcp_servers: - description: IPv4 input for dhcp server ip example 1.1.1.1. + description: Specifies the IPv4 addresses for DHCP servers, for example, "1.1.1.1". elements: str type: list ipv4_dns_servers: - description: IPv4 input for dns server ip example 4.4.4.4. + description: Specifies the IPv4 addresses for DNS servers, for example, "4.4.4.4". elements: str type: list ipv4_gateway: - description: Gateway ip address details, example 175.175.0.1. + description: Provides the gateway's IPv4 address, for example, "175.175.0.1". type: str version_added: 4.0.0 ipv4_global_pool: @@ -119,26 +121,33 @@ description: The ipv4 prefix length is required when ipv4_prefix value is true. type: int ipv4_subnet: - description: IPv4 Subnet address, example 175.175.0.0. + description: Indicates the IPv4 subnet address, for example, "175.175.0.0". type: str ipv4_total_host: - description: IPv4 total host is required when ipv4_prefix value is false. + description: The total number of hosts for IPv4, required when the 'ipv4_prefix' is set to false. type: int ipv6_address_space: description: > - If the value is false only ipv4 input are required, otherwise both - ipv6 and ipv4 are required. + Determines whether both IPv6 and IPv4 inputs are required. + If set to false, only IPv4 inputs are required. + If set to true, both IPv6 and IPv4 inputs are required. type: bool ipv6_dhcp_servers: - description: IPv6 format dhcp server as input example 2001 db8 1234. + description: > + Specifies the IPv6 addresses for DHCP servers in the format. + For example, "2001:0db8:0123:4567:89ab:cdef:0001:0001". elements: str type: list ipv6_dns_servers: - description: IPv6 format dns server input example 2001 db8 1234. + description: > + Specifies the IPv6 addresses for DNS servers. + For example, "2001:0db8:0123:4567:89ab:cdef:0002:0002". elements: str type: list ipv6_gateway: - description: Gateway ip address details, example 2001 db8 85a3 0 100 1. + description: > + Provides the gateway's IPv6 address. + For example, "2001:0db8:0123:4567:89ab:cdef:0003:0003". type: str ipv6_global_pool: description: > @@ -157,23 +166,25 @@ description: IPv6 Subnet address, example 2001 db8 85a3 0 100. type: str ipv6_total_host: - description: IPv6 total host is required when ipv6_prefix value is false. + description: The total number of hosts for IPv6 is required if the 'ipv6_prefix' is set to false. type: int name: - description: Name of the reserve ip sub pool. + description: Name of the reserve IP subpool. type: str prev_name: - description: Previous name of the reserve ip sub pool. + description: The former name associated with the reserved IP sub-pool. type: str site_name: - description: Site name path parameter. Site name to reserve the ip sub pool. + description: > + The name of the site provided as a path parameter, used + to specify where the IP sub-pool will be reserved. type: str slaac_support: description: > - Enables devices in IPv6 networks to automatically - configure their addresses without manual intervention. + Allows devices on IPv6 networks to self-configure their + IP addresses autonomously, eliminating the need for manual setup. type: bool - type: + pool_type: description: Type of the reserve ip sub pool. Generic - Used for general purpose within the network such as device management or communication between the network devices. @@ -301,7 +312,9 @@ description: Input for time zone (eg Africa/Abidjan). type: str site_name: - description: Site name path parameter. + description: > + The name of the site provided as a path parameter, used + to specify where the IP sub-pool will be reserved. type: str requirements: - dnacentersdk == 2.4.5 @@ -349,7 +362,7 @@ gateway: string ip_address_space: string cidr: string - type: Generic + pool_type: Generic dhcp_server_ips: list dns_server_ips: list reserve_pool_details: @@ -365,7 +378,7 @@ ipv6_subnet: string site_name: string slaac_support: True - type: LAN + pool_type: LAN network_management_details: settings: dhcp_server: list @@ -492,33 +505,38 @@ def validate_input(self): "gateway": {"type": 'string'}, "cidr": {"type": 'string'}, "name": {"type": 'string'}, - "prevName": {"type": 'string'}, + "prev_name": {"type": 'string'}, + "pool_type": {"type": 'string', "choices": ["Generic", "Tunnel"]}, } } }, "reserve_pool_details": { "type": 'dict', "name": {"type": 'string'}, - "prevName": {"type": 'string'}, + "prev_name": {"type": 'string'}, "ipv6_address_space": {"type": 'bool'}, "ipv4_global_pool": {"type": 'string'}, "ipv4_prefix": {"type": 'bool'}, "ipv4_prefix_length": {"type": 'string'}, "ipv4_subnet": {"type": 'string'}, - "ipv4GateWay": {"type": 'string'}, - "ipv4DhcpServers": {"type": 'list'}, + "ipv4_gateway": {"type": 'string'}, + "ipv4_dhcp_servers": {"type": 'list'}, "ipv4_dns_servers": {"type": 'list'}, "ipv6_global_pool": {"type": 'string'}, "ipv6_prefix": {"type": 'bool'}, "ipv6_prefix_length": {"type": 'integer'}, "ipv6_subnet": {"type": 'string'}, - "ipv6GateWay": {"type": 'string'}, - "ipv6DhcpServers": {"type": 'list'}, - "ipv6DnsServers": {"type": 'list'}, - "ipv4TotalHost": {"type": 'integer'}, - "ipv6TotalHost": {"type": 'integer'}, + "ipv6_gateway": {"type": 'string'}, + "ipv6_dhcp_servers": {"type": 'list'}, + "ipv6_dns_servers": {"type": 'list'}, + "ipv4_total_host": {"type": 'integer'}, + "ipv6_total_host": {"type": 'integer'}, "slaac_support": {"type": 'bool'}, "site_name": {"type": 'string'}, + "pool_type": { + "type": 'string', + "choices": ["Generic", "LAN", "Management", "Service", "WAN"] + }, }, "network_management_details": { "type": 'dict', @@ -1272,7 +1290,7 @@ def get_want_global_pool(self, global_ippool): "ipPoolName": global_ippool.get("name"), "ipPoolCidr": global_ippool.get("cidr"), "gateway": global_ippool.get("gateway"), - "type": global_ippool.get("type"), + "type": global_ippool.get("pool_type"), }] } } @@ -1329,7 +1347,7 @@ def get_want_reserve_pool(self, reserve_pool): want_reserve = { "name": reserve_pool.get("name"), - "type": reserve_pool.get("type"), + "type": reserve_pool.get("pool_type"), "ipv6AddressSpace": reserve_pool.get("ipv6_address_space"), "ipv4GlobalPool": reserve_pool.get("ipv4_global_pool"), "ipv4Prefix": reserve_pool.get("ipv4_prefix"), diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index a0c134d99b..f4f40c667c 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1355,9 +1355,9 @@ def validate_input(self): 'device_types': { 'type': 'list', 'elements': 'dict', - 'productFamily': {'type': 'str'}, - 'productSeries': {'type': 'str'}, - 'productType': {'type': 'str'}, + 'product_family': {'type': 'str'}, + 'product_series': {'type': 'str'}, + 'product_type': {'type': 'str'}, }, 'failure_policy': {'type': 'str'}, 'id': {'type': 'str'}, diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index 67f665190f..e5a6428d47 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -1355,9 +1355,9 @@ def validate_input(self): 'device_types': { 'type': 'list', 'elements': 'dict', - 'productFamily': {'type': 'str'}, - 'productSeries': {'type': 'str'}, - 'productType': {'type': 'str'}, + 'product_family': {'type': 'str'}, + 'product_series': {'type': 'str'}, + 'product_type': {'type': 'str'}, }, 'failure_policy': {'type': 'str'}, 'id': {'type': 'str'}, From 6eed2c3a95043ca86f009570ec8f66f2ca423146 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 15 Feb 2024 21:42:52 +0530 Subject: [PATCH 35/64] Give the brief description of each parameter in site intent and workflow manager module and explain things in detail like type of rf_model in floor site etc. --- playbooks/site_workflow_manager.yml | 4 +- plugins/modules/site_intent.py | 57 +++++++++++++----------- plugins/modules/site_workflow_manager.py | 56 +++++++++++++---------- 3 files changed, 66 insertions(+), 51 deletions(-) diff --git a/playbooks/site_workflow_manager.yml b/playbooks/site_workflow_manager.yml index 1c4b52ac8a..939e8e7f99 100644 --- a/playbooks/site_workflow_manager.yml +++ b/playbooks/site_workflow_manager.yml @@ -13,9 +13,9 @@ dnac_port: "{{dnac_port}}" dnac_version: "{{dnac_version}}" dnac_debug: "{{dnac_debug}}" - dnac_log: true + dnac_log: True dnac_log_level: DEBUG - config_verify: true + config_verify: True state: merged config: - site: diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index e5ff977f4b..6c190ed758 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -35,50 +35,48 @@ choices: [ merged, deleted ] default: merged config: - description: - - List of details of site being managed. + description: It represents a list of details for creating/managing/deleting sites, including areas, buildings, and floors. type: list elements: dict - required: true + required: True suboptions: type: - description: Type of site to create/update/delete (eg area, building, floor). + description: Specifies the type of site operation to perform (e.g. create, update, delete). type: str site: - description: Site Details. + description: Contains details about the site being managed including areas, buildings and floors. type: dict suboptions: area: - description: Site Create's area. + description: Contains details for creating or managing an area within a site. type: dict suboptions: name: description: Name of the area (eg Area1). type: str - parentName: - description: Complete Parent name of the Area to be created/deleted(eg Global/). + parent_name: + description: Complete Parent name of the Area to be created/deleted(eg Global/USA). type: str building: - description: Building Details. + description: Contains details for creating or managing a building within a site. type: dict suboptions: address: description: Address of the building to be created. type: str latitude: - description: Latitude coordinate of the building (eg 37.338).Values between -90 to +90. - type: int + description: Latitude coordinate of the building (eg 37.338). Values between -90 to +90. longitude: - description: Longitude coordinate of the building (eg -121.832).Values between -180 to +180. + description: Longitude coordinate of the building (eg -121.832). Values between -180 to +180. type: int name: description: Name of the building (eg building1). type: str parent_name: - description: Complete Parent name of the Building to be created/deleted(eg Global/USA/San Francisco). + description: Complete parent name of the building to be created/deleted(eg Global/USA/San Francisco). type: str floor: - description: Site Create's floor. + description: Contains details for creating or managing a floor within a site. type: dict suboptions: height: @@ -90,18 +88,27 @@ name: description: Name of the floor (eg floor-1). type: str - parentName: - description: Complete Parent name of the floor to be created(eg Global/USA/San Francisco/BGL_18). + parent_name: + description: Complete parent name of the floor to be created(eg Global/USA/San Francisco/BGL_18). type: str rf_model: - description: Type of floor. Allowed values are 'Cubes And Walled Offices', - 'Drywall Office Only', 'Indoor High Ceiling', 'Outdoor Open Space'. + description: Type of floor (allowed values are 'Cubes And Walled Offices', 'Drywall Office Only', 'Indoor High Ceiling', + 'Outdoor Open Space'). It refers to the Radio Frequency (RF) model of the floor. It is essential in wireless + networking to simulate and optimize radio signal propagation and coverage within a physical space. + Cubes And Walled Offices - This RF model typically represents indoor areas with cubicles or walled offices, where + radio signals may experience attenuation due to walls and obstacles. + Drywall Office Only - This RF model indicates an environment with drywall partitions, commonly found in office spaces, + which may have moderate signal attenuation. + Indoor High Ceiling - This RF model is suitable for indoor spaces with high ceilings, such as auditoriums or atriums, + where signal propagation may differ due to the height of the ceiling. + Outdoor Open Space - This RF model is used for outdoor areas with open spaces, where signal propagation is less obstructed + and may follow different patterns compared to indoor environments. type: str width: description: Width of the floor units is ft. (eg 100). type: int floor_number: - description: Floor number in the building/site (eg 5).once created, it can't be modified. + description: Floor number in the building/site (eg 5) can be given only while creating the floor site. type: int requirements: @@ -136,7 +143,7 @@ - site: area: name: string - parentName: string + parent_name: string type: string - name: Create a new building site @@ -158,7 +165,7 @@ latitude: 0 longitude: 0 name: string - parentName: string + parent_name: string type: string - name: Create a Floor site under the building @@ -207,7 +214,7 @@ height: int type: string -- name: Deleting any site you need site name and parentName +- name: Deleting any site you need site name and parent name cisco.dnac.site_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -597,9 +604,9 @@ def is_area_updated(self, updated_site, requested_site): - updated_site (dict): The site details after the update. - requested_site (dict): The site details as requested for the update. Return: - bool: True if the area details (name and parentName) have been updated, False otherwise. + bool: True if the area details (name and parent name) have been updated, False otherwise. Description: - This method compares the area details (name and parentName) of the updated site + This method compares the area details (name and parent name) of the updated site with the requested site and returns True if they are equal, indicating that the area details have been updated. Returns False if there is a mismatch in the area site details. """ @@ -620,7 +627,7 @@ def is_building_updated(self, updated_site, requested_site): bool: True if the building details have been updated, False otherwise. Description: This method compares the building details of the updated site with the requested site. - It checks if the name, parentName, latitude, longitude, and address (if provided) are + It checks if the name, parent_name, latitude, longitude, and address (if provided) are equal, indicating that the building details have been updated. Returns True if the details match, and False otherwise. """ diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index 3c0974c222..4c4514c8ef 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -35,50 +35,49 @@ choices: [ merged, deleted ] default: merged config: - description: - - List of details of site being managed. + description: It represents a list of details for creating/managing/deleting sites, including areas, buildings, and floors. type: list elements: dict - required: true + required: True suboptions: type: - description: Type of site to create/update/delete (eg area, building, floor). + description: Specifies the type of site operation to perform (e.g. create, update, delete). type: str site: - description: Site Details. + description: Contains details about the site being managed including areas, buildings and floors. type: dict suboptions: area: - description: Site Create's area. + description: Contains details for creating or managing an area within a site. type: dict suboptions: name: description: Name of the area (eg Area1). type: str - parentName: - description: Complete Parent name of the Area to be created/deleted(eg Global/). + parent_name: + description: Complete Parent name of the Area to be created/deleted(eg Global/USA). type: str building: - description: Building Details. + description: Contains details for creating or managing a building within a site. type: dict suboptions: address: description: Address of the building to be created. type: str latitude: - description: Latitude coordinate of the building (eg 37.338).Values between -90 to +90. + description: Latitude coordinate of the building (eg 37.338). Values between -90 to +90. type: int longitude: - description: Longitude coordinate of the building (eg -121.832).Values between -180 to +180. + description: Longitude coordinate of the building (eg -121.832). Values between -180 to +180. type: int name: description: Name of the building (eg building1). type: str parent_name: - description: Complete Parent name of the Building to be created/deleted(eg Global/USA/San Francisco). + description: Complete parent name of the building to be created/deleted(eg Global/USA/San Francisco). type: str floor: - description: Site Create's floor. + description: Contains details for creating or managing a floor within a site. type: dict suboptions: height: @@ -90,18 +89,27 @@ name: description: Name of the floor (eg floor-1). type: str - parentName: - description: Complete Parent name of the floor to be created(eg Global/USA/San Francisco/BGL_18). + parent_name: + description: Complete parent name of the floor to be created(eg Global/USA/San Francisco/BGL_18). type: str rf_model: - description: Type of floor. Allowed values are 'Cubes And Walled Offices', - 'Drywall Office Only', 'Indoor High Ceiling', 'Outdoor Open Space'. + description: Type of floor (allowed values are 'Cubes And Walled Offices', 'Drywall Office Only', 'Indoor High Ceiling', + 'Outdoor Open Space'). It refers to the Radio Frequency (RF) model of the floor. It is essential in wireless + networking to simulate and optimize radio signal propagation and coverage within a physical space. + Cubes And Walled Offices - This RF model typically represents indoor areas with cubicles or walled offices, where + radio signals may experience attenuation due to walls and obstacles. + Drywall Office Only - This RF model indicates an environment with drywall partitions, commonly found in office spaces, + which may have moderate signal attenuation. + Indoor High Ceiling - This RF model is suitable for indoor spaces with high ceilings, such as auditoriums or atriums, + where signal propagation may differ due to the height of the ceiling. + Outdoor Open Space - This RF model is used for outdoor areas with open spaces, where signal propagation is less obstructed + and may follow different patterns compared to indoor environments. type: str width: description: Width of the floor units is ft. (eg 100). type: int floor_number: - description: Floor number in the building/site (eg 5).once created, it can't be modified. + description: Floor number in the building/site (eg 5) can be given only while creating the floor site. type: int requirements: @@ -136,7 +144,7 @@ - site: area: name: string - parentName: string + parent_name: string type: string - name: Create a new building site @@ -158,7 +166,7 @@ latitude: 0 longitude: 0 name: string - parentName: string + parent_name: string type: string - name: Create a Floor site under the building @@ -207,7 +215,7 @@ height: int type: string -- name: Deleting any site you need site name and parentName +- name: Deleting any site you need site name and parent name cisco.dnac.site_workflow_manager: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -596,9 +604,9 @@ def is_area_updated(self, updated_site, requested_site): - updated_site (dict): The site details after the update. - requested_site (dict): The site details as requested for the update. Return: - bool: True if the area details (name and parentName) have been updated, False otherwise. + bool: True if the area details (name and parent name) have been updated, False otherwise. Description: - This method compares the area details (name and parentName) of the updated site + This method compares the area details (name and parent name) of the updated site with the requested site and returns True if they are equal, indicating that the area details have been updated. Returns False if there is a mismatch in the area site details. """ @@ -619,7 +627,7 @@ def is_building_updated(self, updated_site, requested_site): bool: True if the building details have been updated, False otherwise. Description: This method compares the building details of the updated site with the requested site. - It checks if the name, parentName, latitude, longitude, and address (if provided) are + It checks if the name, parent_name, latitude, longitude, and address (if provided) are equal, indicating that the building details have been updated. Returns True if the details match, and False otherwise. """ From 67b7a182500cb455400d036fcb0d0faf6f278680 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 15 Feb 2024 22:23:43 +0530 Subject: [PATCH 36/64] make site type description more clearer with examples --- plugins/modules/site_intent.py | 2 +- plugins/modules/site_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 6c190ed758..578e4cde4e 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -41,7 +41,7 @@ required: True suboptions: type: - description: Specifies the type of site operation to perform (e.g. create, update, delete). + description: Type of site to create/update/delete (eg area, building, floor). type: str site: description: Contains details about the site being managed including areas, buildings and floors. diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index 4c4514c8ef..dd1e41c9a4 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -41,7 +41,7 @@ required: True suboptions: type: - description: Specifies the type of site operation to perform (e.g. create, update, delete). + description: Type of site to create/update/delete (eg area, building, floor). type: str site: description: Contains details about the site being managed including areas, buildings and floors. From e09cac95b3b81504c98dfaa63346785d8fc5f3eb Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 15 Feb 2024 16:54:35 +0000 Subject: [PATCH 37/64] Making changes to incorporate discovery specific credentials --- playbooks/discovery_intent.yml | 49 +- playbooks/discovery_workflow_manager.yml | 86 ++++ plugins/modules/discovery_intent.py | 473 +++++++++++------- plugins/modules/discovery_workflow_manager.py | 473 +++++++++++------- 4 files changed, 723 insertions(+), 358 deletions(-) create mode 100644 playbooks/discovery_workflow_manager.yml diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index b8917b4423..037b247504 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -1,5 +1,5 @@ --- -- name: Discover devices +- name: Discover devices using multiple discovery specific credentials and delete all the discoveries hosts: localhost connection: local gather_facts: no @@ -20,21 +20,44 @@ dnac_log_level: DEBUG tasks: - - name: Execute discovery devices using MULTI RANGE + - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials cisco.dnac.discovery_intent: <<: *dnac_login state: merged config_verify: True config: - ip_address_list: - - 204.1.2.1 #It will be taken as 204.1.2.1 - 204.1.2.1 + - 204.1.2.1-204.1.2.100 #It will be taken as 204.1.2.1 - 204.1.2.1 - 205.2.1.1-205.2.1.10 + ip_filter_list: + - 204.1.2.5 #Devie with IP 204.1.2.5 won't be discovered + discovery_specific_credentials: + cli_credentials_list: + - username: admin + password: maglev123 + enable_password: maglev123 + http_read_credential: + username: admin + password: maglev123 + port: 443 + secure: True + snmp_v2_read_credential: + desc: new + community: password + snmp_v3_credential: + username: administrator + snmp_mode: AUTHPRIV + auth_password: admin123 + auth_type: SHA + privacy_type: AES192 + privacy_password: cisco#123 discovery_type: "MULTI RANGE" - discovery_name: File_111 + discovery_name: Multi_range protocol_order: ssh start_index: 1 - records_to_return: 25 + records_to_return: 1000 snmp_version: v2 + global_cli_len: 5 - name: Execute discovery devices using CDP/LLDP/CIDR cisco.dnac.discovery_intent: @@ -47,7 +70,17 @@ discovery_type: "CDP" #Can be LLDP and CIDR cdp_level: 16 #Instead use lldp for LLDP and prefix length for CIDR discovery_name: CDP_Test_1 + discovery_specific_credentials: + cli_credentials_list: + - username: admin + password: maglev123 + enable_password: maglev123 protocol_order: ssh - start_index: 1 - records_to_return: 25 - snmp_version: v2 \ No newline at end of file + + - name: Execute deletion of all the discoveries from the dashboard + cisco.dnac.discovery_intent: + <<: *dnac_login + state: deleted + config_verify: True + config: + - delete_all: True \ No newline at end of file diff --git a/playbooks/discovery_workflow_manager.yml b/playbooks/discovery_workflow_manager.yml new file mode 100644 index 0000000000..3ae75fb3bc --- /dev/null +++ b/playbooks/discovery_workflow_manager.yml @@ -0,0 +1,86 @@ +--- +- name: Discover devices using multiple discovery specific credentials and delete all the discoveries + hosts: localhost + connection: local + gather_facts: no + + vars_files: + - "{{ CLUSTERFILE }}" + + vars: + dnac_login: &dnac_login + dnac_host: "{{ dnac_host }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_port: "{{ dnac_port }}" + dnac_version: "{{ dnac_version }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + dnac_log_level: DEBUG + + tasks: + - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials + cisco.dnac.discovery_workflow_manager: + <<: *dnac_login + state: merged + config_verify: True + config: + - ip_address_list: + - 204.1.2.1-204.1.2.100 #It will be taken as 204.1.2.1 - 204.1.2.1 + - 205.2.1.1-205.2.1.10 + ip_filter_list: + - 204.1.2.5 #Devie with IP 204.1.2.5 won't be discovered + discovery_specific_credentials: + cli_credentials_list: + - username: admin + password: maglev123 + enable_password: maglev123 + http_read_credential: + username: admin + password: maglev123 + port: 443 + secure: True + snmp_v2_read_credential: + desc: new + community: password + snmp_v3_credential: + username: administrator + snmp_mode: AUTHPRIV + auth_password: admin123 + auth_type: SHA + privacy_type: AES192 + privacy_password: cisco#123 + discovery_type: "MULTI RANGE" + discovery_name: Multi_range + protocol_order: ssh + start_index: 1 + records_to_return: 1000 + snmp_version: v2 + global_cli_len: 5 + + - name: Execute discovery devices using CDP/LLDP/CIDR + cisco.dnac.discovery_workflow_manager: + <<: *dnac_login + state: merged + config_verify: True + config: + - ip_address_list: #List length should be one + - 204.1.2.1 + discovery_type: "CDP" #Can be LLDP and CIDR + cdp_level: 16 #Instead use lldp for LLDP and prefix length for CIDR + discovery_name: CDP_Test_1 + discovery_specific_credentials: + cli_credentials_list: + - username: admin + password: maglev123 + enable_password: maglev123 + protocol_order: ssh + + - name: Execute deletion of all the discoveries from the dashboard + cisco.dnac.discovery_workflow_manager: + <<: *dnac_login + state: deleted + config_verify: True + config: + - delete_all: True \ No newline at end of file diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 2b44dae2b0..0a55ab1cc2 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -13,7 +13,7 @@ module: discovery_intent short_description: Resource module for discovery related functions description: -- Manage operations discover devices using IP address/range, CDP, LLDP and delete discoveries +- Manage operations disicover devices using IP address/range, CDP, LLDP and delete discoveries - API to discover a device or multiple devices - API to delete a discovery of a device or multiple devices version_added: '6.6.0' @@ -49,9 +49,10 @@ elements: str required: true discovery_type: - description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP) + description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) type: str required: true + choices: [ 'SINGLE', 'RANGE', 'MULTI RANGE', 'CDP', 'LLDP', 'CIDR'] cdp_level: description: Total number of levels that are there in cdp's method of discovery type: int @@ -64,65 +65,137 @@ description: Start index for the header in fetching SNMP v2 credentials type: int default: 1 - enable_password_list: - description: List of enable passwords for the CLI crfedentials - type: list - elements: str records_to_return: description: Number of records to return for the header in fetching global v2 credentials type: int default: 100 - http_read_credential: - description: HTTP read credentials for hosting a device + discovery_specific_credentials: + description: Credentials specifically created by the user while performing discovery. type: dict suboptions: - username: - description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. - type: str - password: - description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + cli_credentials_list: + description: List of CLI credentials to be used while performing discovery. + type: list + elements: dict + suboptions: + username: + description: The username for CLI authentication, which is mandatory when using CLI credential. + type: str + password: + description: The password for CLI authentication, which is mandatory when using CLI credential. + type: str + enable_password: + description: The enable password for CLI authentication, which is mandatory when using CLI credential. + type: str + http_read_credential: + description: HTTP read credentials for hosting a device + type: dict + suboptions: + username: + description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + type: str + password: + description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + type: str + port: + description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + type: int + secure: + description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + type: bool + http_write_credential: + description: HTTP write credentials for hosting a device + type: dict + suboptions: + username: + description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + type: str + password: + description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + type: str + port: + description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + type: int + secure: + description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + type: bool + snmp_v2_read_credential: + description: + - The SNMP v2 credentials to be created and used for contacting a device via SNMP protocol in read mode. + - SNMP v2 also delivers data encryptions, but it uses data types. + type: dict + suboptions: + desc: + descritption: Name/Description of the SNMP read credential to be used for creation of snmp_v2_read_credential. + type: str + community: + descritption: The read-only community string enables Cisco Catalyst Center to extract read-only data from device. + type: str + snmp_v2_write_credential: + description: + - The SNMP v2 credentials to be created and used for contacting a device via SNMP protocol in read and write mode. + - SNMP v2 also delivers data encryptions, but it uses data types. + type: dict + suboptions: + desc: + descritption: Name/Description of the SNMP write credential to be used for creation of snmp_v2_write_credential. + type: str + community: + descritption: The read-write community string is used to extract data and alter device configurations. + type: str + snmp_v3_credential: + description: + - The SNMP v3 credentials to be created and used for contacting a device via SNMP protocol in read and write mode. + - SNMPv3 is the most secure version of SNMP, allowing users to fully encrypt transmissions, keeping us safe from external attackers. + type: dict + suboptions: + username: + description: Username of the SNMP v3 protocol to be used. + type: str + snmp_mode: + description: Mode of SNMP which determines the encryption level of our community string. + type: str + choices: [ 'AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV' ] + auth_password: + description: + - Authentiaction Password of the SNMP v3 protocol to be used. + - Must be of length greater than 7 characters. + - Not required for NOAUTHNOPRIV snmp_mode. + type: str + auth_type: + description: + - Authentication type of the SNMP v3 protocol to be used. + - SHA uses Secure Hash Algorithm (SHA) as your authentication protocol. + - MD5 uses Message Digest 5 (MD5) as your authentication protocol and is not recommended. + - Not required for NOAUTHNOPRIV snmp_mode. + type: str + choices: [ 'SHA', 'MD5' ] + privacy_type: + description: + - Privacy type/protocol of the SNMP v3 protocol to be used in AUTHPRIV SNMP mode + - Not required for 'AUTHNOPRIV' and NOAUTHNOPRIV snmp_mode. + type: str + choices: [ 'AES128', 'AES192', 'AES256' ] + privacy_password: + description: + - Privacy password of the SNMP v3 protocol to be used in AUTHPRIV SNMP mode + - Not required for 'AUTHNOPRIV' and NOAUTHNOPRIV snmp_mode. + type: str + net_conf_port: + description: + - To be used when network contains IOS XE-based wireless controllers. + - This is used for discovery and the enabling of wireless services on the controllers. + - Requires valid SSH credentials to work. + - Standard ports like 22, 80, 8080 must be avoided. type: str - port: - description: The HTTP(S) port number, which is mandatory for using HTTP credentials. - type: int - secure: - description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. - type: bool - http_write_credential: - description: HTTP write credentials for hosting a device - type: dict - suboptions: - username: - description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. - type: str - password: - description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. - type: str - port: - description: The HTTP(S) port number, which is mandatory for using HTTP credentials. - type: int - secure: - description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. - type: bool ip_filter_list: - description: List of IP adddrsess that needs to get filtered out from the IP addresses added + description: List of IP adddrsess that needs to get filtered out from the IP addresses passed. type: list elements: str discovery_name: - description: Name of the discovery task + description: Name of the discovery taski type: str required: true - netconf_port: - description: Port for the netconf credentials - type: str - password_list: - description: List of passwords for the CLI credentials - type: list - elements: str - username_list: - description: List of passwords for the CLI credentials - type: list - elements: str preferred_mgmt_ip_method: description: Preferred method for the management of the IP (None/UseLoopBack) type: str @@ -131,46 +204,14 @@ description: Order of protocol (ssh/telnet) in which device connection will be tried. For example, 'telnet' - only telnet - 'ssh, telnet' - ssh with higher order than telnet type: str + required: true retry: description: Number of times to try establishing connection to device type: int - snmp_auth_passphrase: - description: Auth Pass phrase for SNMP - type: str - snmp_auth_protocol: - description: SNMP auth protocol (SHA/MD5) - type: str - snmp_mode: - description: Mode of SNMP (AUTHPRIV/AUTHNOPRIV/NOAUTHNOPRIV) - type: str - snmp_priv_passphrase: - description: Pass phrase for SNMP privacy - type: str - snmp_priv_protocol: - description: SNMP privacy protocol (DES/AES128) - type: str - snmp_ro_community: - description: Snmp RO community of the devices to be discovered - type: str - snmp_ro_community_desc: - description: Description for Snmp RO community - type: str - snmp_rw_community: - description: Snmp RW community of the devices to be discovered - type: str - snmp_rw_community_desc: - description: Description for Snmp RW community - type: str - snmp_username: - description: SNMP username of the device - type: str - snmp_version: - description: Version of SNMP (v2/v3) - type: str timeout: description: Time to wait for device response in seconds type: int - cli_cred_len: + global_cli_len: description: Specifies the total number of CLI credentials to be used, ranging from 1 to 5. type: int default: 1 @@ -224,30 +265,43 @@ cdp_level: string lldp_level: string start_index: integer - enable_password_list: list records_to_return: integer - http_read_credential: dictionary - http_write_credential: dictionary ip_filter_list: list discovery_name: string password_list: list - preffered_mgmt_ip_method: string + prefered_mgmt_ip_method: string protocol_order: string retry: integer - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string - snmp_ro_community: string - snmp_ro_community_desc: string - snmp_rw_community: string - snmp_rw_community_desc: string - snmp_username: string - snmp_version: string timeout: integer - username_list: list - cli_cred_len: integer + global_cli_len: integer + discovery_specific_credentials: + cli_credentials_list: + - username: string + password: string + enable_password: string + http_read_credential: + username: string + password: string + port: integer + secure: boolean + http_write_credential: + username: string + password: string + port: integer + secure: boolean + snmp_v2_read_credential: + desc: string + community: string + snmp_v2_write_credential: + desc: string + community: string + snmp_v3_credential: + username: string + snmp_mode: string + auth_password: string + auth_type: string + privacy_type: string + privacy_password: string - name: Delete disovery by name cisco.dnac.discovery_intent: @@ -361,48 +415,30 @@ def validate_input(self, state=None): discovery_spec = { 'cdp_level': {'type': 'int', 'required': False, 'default': 16}, - 'enable_password_list': {'type': 'list', 'required': False, - 'elements': 'str'}, 'start_index': {'type': 'int', 'required': False, 'default': 1}, 'records_to_return': {'type': 'int', 'required': False, 'default': 100}, - 'http_read_credential': {'type': 'dict', 'required': False}, - 'http_write_credential': {'type': 'dict', 'required': False}, + 'discovery_specific_credentials': {'type': 'dict', 'required': False}, 'ip_filter_list': {'type': 'list', 'required': False, 'elements': 'str'}, 'lldp_level': {'type': 'int', 'required': False, 'default': 16}, 'discovery_name': {'type': 'str', 'required': True}, 'netconf_port': {'type': 'str', 'required': False}, - 'password_list': {'type': 'list', 'required': False, - 'elements': 'str'}, 'preferred_mgmt_ip_method': {'type': 'str', 'required': False, 'default': 'None'}, - 'protocol_order': {'type': 'str', 'required': False}, 'retry': {'type': 'int', 'required': False}, - 'snmp_auth_passphrase': {'type': 'str', 'required': False}, - 'snmp_auth_protocol': {'type': 'str', 'required': False}, - 'snmp_mode': {'type': 'str', 'required': False}, - 'snmp_priv_passphrase': {'type': 'str', 'required': False}, - 'snmp_priv_protocol': {'type': 'str', 'required': False}, - 'snmp_ro_community': {'type': 'str', 'required': False}, - 'snmp_ro_community_desc': {'type': 'str', 'required': False}, - 'snmp_rw_community': {'type': 'str', 'required': False}, - 'snmp_rw_community_desc': {'type': 'str', 'required': False}, - 'snmp_username': {'type': 'str', 'required': False}, - 'snmp_version': {'type': 'str', 'required': False}, 'timeout': {'type': 'str', 'required': False}, - 'username_list': {'type': 'list', 'required': False, - 'elements': 'str'}, - 'cli_cred_len': {'type': 'int', 'required': False, - 'default': 1} + 'global_cli_len': {'type': 'int', 'required': False, + 'default': 1} } if state == "merged": discovery_spec["ip_address_list"] = {'type': 'list', 'required': True, 'elements': 'str'} discovery_spec["discovery_type"] = {'type': 'str', 'required': True} + discovery_spec["protocol_order"] = {'type': 'str', 'required': True} elif state == "deleted": if self.config[0].get("delete_all") is True: @@ -463,7 +499,7 @@ def get_ccc_global_credentials_v2_info(self): response = response.get('response') self.log("The Global credentials response from 'get all global credentials v2' API is {0}".format(str(response)), "DEBUG") - cli_len_inp = self.validated_config[0].get("cli_cred_len") + cli_len_inp = self.validated_config[0].get("global_cli_len") if response.get("cliCredential") is None: msg = 'Not found any CLI credentials to perform discovery' self.log(msg, "CRITICAL") @@ -580,105 +616,179 @@ def preprocess_device_discovery_handle_error(self): self.log("IP Address list's length is longer than 1", "ERROR") self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) - def http_cred_failure(self, msg=None): + def discovery_specific_cred_failure(self, msg=None): """ Method for failing discovery if there is any discrepancy in the http credentials passed by the user """ + self.log(msg, "CRITICAL") self.module.fail_json(msg=msg) - def create_params(self, credential_ids=None, ip_address_list=None): + def handle_discovery_specific_credentials(self, new_object_params=None): """ - Create a new parameter object based on the validated configuration, - credential IDs, and IP address list. + Method to convert values for create_params API when discovery specific paramters + are passed as input. Parameters: - - credential_ids: The list of credential IDs to include in the - parameters. If not provided, an empty list is used. - - ip_address_list: The list of IP addresses to include in the - parameters. If not provided, None is used. + - new_object_params: The dictionary storing various parameters for calling the + start discovery API Returns: - - new_object_params: A dictionary containing the newly created - parameters. + - new_object_params: The dictionary storing various parameters for calling the + start discovery API in an updated fashion """ - if credential_ids is None: - credential_ids = [] + discovery_specific_credentials = self.validated_config[0].get('discovery_specific_credentials') + cli_credentials_list = discovery_specific_credentials.get('cli_credentials_list') + http_read_credential = discovery_specific_credentials.get('http_read_credential') + http_write_credential = discovery_specific_credentials.get('http_write_credential') + snmp_v2_read_credential = discovery_specific_credentials.get('snmp_v2_read_credential') + snmp_v2_write_credential = discovery_specific_credentials.get('snmp_v2_write_credential') + snmp_v3_credential = discovery_specific_credentials.get('snmp_v3_credential') + net_conf_port = discovery_specific_credentials.get('net_conf_port') + + if cli_credentials_list: + if not isinstance(cli_credentials_list, list): + msg = "Device Specific ClI credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(cli_credentials_list) > 0: + username_list = [] + password_list = [] + enable_password_list = [] + for cli_cred in cli_credentials_list: + if cli_cred.get('username') and cli_cred.get('password') and cli_cred.get('enable_password'): + username_list.append(cli_cred.get('username')) + password_list.append(cli_cred.get('password')) + enable_password_list.append(cli_cred.get('enable_password')) + else: + msg = "username, password and enable_password must be passed toether for creating CLI credentials" + self.discovery_specific_cred_failure(msg=msg) + new_object_params['userNameList'] = username_list + new_object_params['passwordList'] = password_list + new_object_params['enablePasswordList'] = enable_password_list - http_read_credential = self.validated_config[0].get('http_read_credential') - http_write_credential = self.validated_config[0].get('http_write_credential') if http_read_credential: if not (http_read_credential.get('password') and isinstance(http_read_credential.get('password'), str)): msg = "The password for the HTTP read credential must be of string type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not (http_read_credential.get('username') and isinstance(http_read_credential.get('username'), str)): msg = "The username for the HTTP read credential must be of string type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not (http_read_credential.get('port') and isinstance(http_read_credential.get('port'), int)): msg = "The port for the HTTP read Credential must be of integer type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not isinstance(http_read_credential.get('secure'), bool): msg = "Secure for HTTP read Credential must be of type boolean." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) + new_object_params['httpReadCredential'] = http_read_credential if http_write_credential: if not (http_write_credential.get('password') and isinstance(http_write_credential.get('password'), str)): msg = "The password for the HTTP write credential must be of string type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not (http_write_credential.get('username') and isinstance(http_write_credential.get('username'), str)): msg = "The username for the HTTP write credential must be of string type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not (http_write_credential.get('port') and isinstance(http_write_credential.get('port'), int)): msg = "The port for the HTTP write Credential must be of integer type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not isinstance(http_write_credential.get('secure'), bool): msg = "Secure for HTTP write Credential must be of type boolean." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) + new_object_params['httpWriteCredential'] = http_write_credential + + if snmp_v2_read_credential: + if not (snmp_v2_read_credential.get('desc')) and isinstance(snmp_v2_read_credential.get('desc'), str): + msg = "Name/description for the SNMP v2 read credential must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v2_read_credential.get('community')) and isinstance(snmp_v2_read_credential.get('community'), str): + msg = "The community string must be of string type" + self.discovery_specific_cred_failure(msg=msg) + new_object_params['snmpROCommunityDesc'] = snmp_v2_read_credential.get('desc') + new_object_params['snmpROCommunity'] = snmp_v2_read_credential.get('community') + new_object_params['snmpVersion'] = "v2" + + if snmp_v2_write_credential: + if not (snmp_v2_write_credential.get('desc')) and isinstance(snmp_v2_write_credential.get('desc'), str): + msg = "Name/description for the SNMP v2 write credential must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v2_write_credential.get('community')) and isinstance(snmp_v2_write_credential.get('community'), str): + msg = "The community string must be of string type" + self.discovery_specific_cred_failure(msg=msg) + new_object_params['snmpRWCommunityDesc'] = snmp_v2_write_credential.get('desc') + new_object_params['snmpRWCommunity'] = snmp_v2_write_credential.get('community') + new_object_params['snmpVersion'] = "v2" + + if snmp_v3_credential: + if not (snmp_v3_credential.get('username')) and isinstance(snmp_v3_credential.get('username'), str): + msg = "Username of SNMP v3 protocol must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v3_credential.get('snmp_mode')) and isinstance(snmp_v3_credential.get('snmp_mode'), str): + msg = "Mode of SNMP is madantory to use SNMPv3 protocol and must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if (snmp_v3_credential.get('snmp_mode')) == "AUTHPRIV" or snmp_v3_credential.get('snmp_mode') == "AUTHNOPRIV": + if not (snmp_v3_credential.get('auth_password')) and isinstance(snmp_v3_credential.get('auth_password'), str): + msg = "Authorization password must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v3_credential.get('auth_type')) and isinstance(snmp_v3_credential.get('auth_type'), str): + msg = "Authorization type must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if snmp_v3_credential.get('snmp_mode') == "AUTHPRIV": + if not (snmp_v3_credential.get('privacy_type')) and isinstance(snmp_v3_credential.get('privacy_type'), str): + msg = "Privacy type must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v3_credential.get('privacy_password')) and isinstance(snmp_v3_credential.get('privacy_password'), str): + msg = "Privacy password must be of string type" + self.discovery_specific_cred_failure(msg=msg) + new_object_params['snmpUserName'] = snmp_v3_credential.get('username') + new_object_params['snmpMode'] = snmp_v3_credential.get('snmp_mode') + new_object_params['snmpAuthPassphrase'] = snmp_v3_credential.get('auth_password') + new_object_params['snmpAuthProtocol'] = snmp_v3_credential.get('auth_type') + new_object_params['snmpPrivProtocol'] = snmp_v3_credential.get('privacy_type') + new_object_params['snmpPrivPassphrase'] = snmp_v3_credential.get('privacy_password') + new_object_params['snmpVersion'] = "v3" + + if net_conf_port: + new_object_params['netconfPort'] = str(net_conf_port) + + return new_object_params + + def create_params(self, credential_ids=None, ip_address_list=None): + """ + Create a new parameter object based on the validated configuration, + credential IDs, and IP address list. + + Parameters: + - credential_ids: The list of credential IDs to include in the + parameters. If not provided, an empty list is used. + - ip_address_list: The list of IP addresses to include in the + parameters. If not provided, None is used. + + Returns: + - new_object_params: A dictionary containing the newly created + parameters. + """ + + if credential_ids is None: + credential_ids = [] new_object_params = {} new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') new_object_params['discoveryType'] = self.validated_config[0].get('discovery_type') - new_object_params['enablePasswordList'] = self.validated_config[0].get( - 'enable_password_list') new_object_params['globalCredentialIdList'] = credential_ids - new_object_params['httpReadCredential'] = self.validated_config[0].get( - 'http_read_credential') - new_object_params['httpWriteCredential'] = self.validated_config[0].get( - 'http_write_credential') new_object_params['ipAddressList'] = ip_address_list new_object_params['ipFilterList'] = self.validated_config[0].get('ip_filter_list') new_object_params['lldpLevel'] = self.validated_config[0].get('lldp_level') new_object_params['name'] = self.validated_config[0].get('discovery_name') - new_object_params['netconfPort'] = self.validated_config[0].get('netconf_port') - new_object_params['passwordList'] = self.validated_config[0].get('password_list') - new_object_params['preferredMgmtIPMethod'] = self.validated_config[0].get( - 'preferred_mgmt_ip_method') + new_object_params['preferredMgmtIPMethod'] = self.validated_config[0].get('preferred_mgmt_ip_method') new_object_params['protocolOrder'] = self.validated_config[0].get('protocol_order') new_object_params['retry'] = self.validated_config[0].get('retry') - new_object_params['snmpAuthPassphrase'] = self.validated_config[0].get( - 'snmp_auth_Passphrase') - new_object_params['snmpAuthProtocol'] = self.validated_config[0].get( - 'snmp_auth_protocol') - new_object_params['snmpMode'] = self.validated_config[0].get('snmp_mode') - new_object_params['snmpPrivPassphrase'] = self.validated_config[0].get( - 'snmp_priv_passphrase') - new_object_params['snmpPrivProtocol'] = self.validated_config[0].get( - 'snmp_priv_protocol') - new_object_params['snmpROCommunity'] = self.validated_config[0].get( - 'snmp_ro_community') - new_object_params['snmpROCommunityDesc'] = self.validated_config[0].get( - 'snmp_ro_community_desc') - new_object_params['snmpRWCommunity'] = self.validated_config[0].get( - 'snmp_rw_community') - new_object_params['snmpRWCommunityDesc'] = self.validated_config[0].get( - 'snmp_rw_community_desc') - new_object_params['snmpUserName'] = self.validated_config[0].get( - 'snmp_username') - new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') new_object_params['timeout'] = self.validated_config[0].get('timeout') - new_object_params['userNameList'] = self.validated_config[0].get('username_list') + + if self.validated_config[0].get('discovery_specific_credentials'): + self.handle_discovery_specific_credentials(new_object_params=new_object_params) + self.log("The payload/object created for calling the start discovery API is {0}".format(str(new_object_params)), "INFO") return new_object_params @@ -1085,6 +1195,19 @@ def verify_diff_deleted(self, config): self.log("Current State (have): {0}".format(str(self.have)), "INFO") self.log("Desired State (want): {0}".format(str(config)), "INFO") # Code to validate Cisco Catalyst Center config for deleted state + if config.get("delete_all") is True: + count_discoveries = self.dnac_apply['exec']( + family="discovery", + function="get_count_of_all_discovery_jobs", + ) + if count_discoveries == 0: + self.log("All discoveries are deleted", "INFO") + + else: + self.log("All discoveries are not deleted", "WARNING") + self.status = "success" + return self + discovery_task_info = self.lookup_discovery_by_range_via_name() discovery_name = config.get('discovery_name') if discovery_task_info: diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 4e088e15da..7100bbb60b 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -13,7 +13,7 @@ module: discovery_workflow_manager short_description: Resource module for discovery related functions description: -- Manage operations discover devices using IP address/range, CDP, LLDP and delete discoveries +- Manage operations disicover devices using IP address/range, CDP, LLDP and delete discoveries - API to discover a device or multiple devices - API to delete a discovery of a device or multiple devices version_added: '6.6.0' @@ -49,9 +49,10 @@ elements: str required: true discovery_type: - description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP) + description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) type: str required: true + choices: [ 'SINGLE', 'RANGE', 'MULTI RANGE', 'CDP', 'LLDP', 'CIDR'] cdp_level: description: Total number of levels that are there in cdp's method of discovery type: int @@ -64,65 +65,137 @@ description: Start index for the header in fetching SNMP v2 credentials type: int default: 1 - enable_password_list: - description: List of enable passwords for the CLI crfedentials - type: list - elements: str records_to_return: description: Number of records to return for the header in fetching global v2 credentials type: int default: 100 - http_read_credential: - description: HTTP read credentials for hosting a device + discovery_specific_credentials: + description: Credentials specifically created by the user while performing discovery. type: dict suboptions: - username: - description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. - type: str - password: - description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + cli_credentials_list: + description: List of CLI credentials to be used while performing discovery. + type: list + elements: dict + suboptions: + username: + description: The username for CLI authentication, which is mandatory when using CLI credential. + type: str + password: + description: The password for CLI authentication, which is mandatory when using CLI credential. + type: str + enable_password: + description: The enable password for CLI authentication, which is mandatory when using CLI credential. + type: str + http_read_credential: + description: HTTP read credentials for hosting a device + type: dict + suboptions: + username: + description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + type: str + password: + description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + type: str + port: + description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + type: int + secure: + description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + type: bool + http_write_credential: + description: HTTP write credentials for hosting a device + type: dict + suboptions: + username: + description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + type: str + password: + description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + type: str + port: + description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + type: int + secure: + description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + type: bool + snmp_v2_read_credential: + description: + - The SNMP v2 credentials to be created and used for contacting a device via SNMP protocol in read mode. + - SNMP v2 also delivers data encryptions, but it uses data types. + type: dict + suboptions: + desc: + descritption: Name/Description of the SNMP read credential to be used for creation of snmp_v2_read_credential. + type: str + community: + descritption: The read-only community string enables Cisco Catalyst Center to extract read-only data from device. + type: str + snmp_v2_write_credential: + description: + - The SNMP v2 credentials to be created and used for contacting a device via SNMP protocol in read and write mode. + - SNMP v2 also delivers data encryptions, but it uses data types. + type: dict + suboptions: + desc: + descritption: Name/Description of the SNMP write credential to be used for creation of snmp_v2_write_credential. + type: str + community: + descritption: The read-write community string is used to extract data and alter device configurations. + type: str + snmp_v3_credential: + description: + - The SNMP v3 credentials to be created and used for contacting a device via SNMP protocol in read and write mode. + - SNMPv3 is the most secure version of SNMP, allowing users to fully encrypt transmissions, keeping us safe from external attackers. + type: dict + suboptions: + username: + description: Username of the SNMP v3 protocol to be used. + type: str + snmp_mode: + description: Mode of SNMP which determines the encryption level of our community string. + type: str + choices: [ 'AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV' ] + auth_password: + description: + - Authentiaction Password of the SNMP v3 protocol to be used. + - Must be of length greater than 7 characters. + - Not required for NOAUTHNOPRIV snmp_mode. + type: str + auth_type: + description: + - Authentication type of the SNMP v3 protocol to be used. + - SHA uses Secure Hash Algorithm (SHA) as your authentication protocol. + - MD5 uses Message Digest 5 (MD5) as your authentication protocol and is not recommended. + - Not required for NOAUTHNOPRIV snmp_mode. + type: str + choices: [ 'SHA', 'MD5' ] + privacy_type: + description: + - Privacy type/protocol of the SNMP v3 protocol to be used in AUTHPRIV SNMP mode + - Not required for 'AUTHNOPRIV' and NOAUTHNOPRIV snmp_mode. + type: str + choices: [ 'AES128', 'AES192', 'AES256' ] + privacy_password: + description: + - Privacy password of the SNMP v3 protocol to be used in AUTHPRIV SNMP mode + - Not required for 'AUTHNOPRIV' and NOAUTHNOPRIV snmp_mode. + type: str + net_conf_port: + description: + - To be used when network contains IOS XE-based wireless controllers. + - This is used for discovery and the enabling of wireless services on the controllers. + - Requires valid SSH credentials to work. + - Standard ports like 22, 80, 8080 must be avoided. type: str - port: - description: The HTTP(S) port number, which is mandatory for using HTTP credentials. - type: int - secure: - description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. - type: bool - http_write_credential: - description: HTTP write credentials for hosting a device - type: dict - suboptions: - username: - description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. - type: str - password: - description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. - type: str - port: - description: The HTTP(S) port number, which is mandatory for using HTTP credentials. - type: int - secure: - description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. - type: bool ip_filter_list: - description: List of IP adddrsess that needs to get filtered out from the IP addresses added + description: List of IP adddrsess that needs to get filtered out from the IP addresses passed. type: list elements: str discovery_name: - description: Name of the discovery task + description: Name of the discovery taski type: str required: true - netconf_port: - description: Port for the netconf credentials - type: str - password_list: - description: List of passwords for the CLI credentials - type: list - elements: str - username_list: - description: List of passwords for the CLI credentials - type: list - elements: str preferred_mgmt_ip_method: description: Preferred method for the management of the IP (None/UseLoopBack) type: str @@ -131,46 +204,14 @@ description: Order of protocol (ssh/telnet) in which device connection will be tried. For example, 'telnet' - only telnet - 'ssh, telnet' - ssh with higher order than telnet type: str + required: true retry: description: Number of times to try establishing connection to device type: int - snmp_auth_passphrase: - description: Auth Pass phrase for SNMP - type: str - snmp_auth_protocol: - description: SNMP auth protocol (SHA/MD5) - type: str - snmp_mode: - description: Mode of SNMP (AUTHPRIV/AUTHNOPRIV/NOAUTHNOPRIV) - type: str - snmp_priv_passphrase: - description: Pass phrase for SNMP privacy - type: str - snmp_priv_protocol: - description: SNMP privacy protocol (DES/AES128) - type: str - snmp_ro_community: - description: Snmp RO community of the devices to be discovered - type: str - snmp_ro_community_desc: - description: Description for Snmp RO community - type: str - snmp_rw_community: - description: Snmp RW community of the devices to be discovered - type: str - snmp_rw_community_desc: - description: Description for Snmp RW community - type: str - snmp_username: - description: SNMP username of the device - type: str - snmp_version: - description: Version of SNMP (v2/v3) - type: str timeout: description: Time to wait for device response in seconds type: int - cli_cred_len: + global_cli_len: description: Specifies the total number of CLI credentials to be used, ranging from 1 to 5. type: int default: 1 @@ -224,30 +265,43 @@ cdp_level: string lldp_level: string start_index: integer - enable_password_list: list records_to_return: integer - http_read_credential: dictionary - http_write_credential: dictionary ip_filter_list: list discovery_name: string password_list: list - preffered_mgmt_ip_method: string + prefered_mgmt_ip_method: string protocol_order: string retry: integer - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string - snmp_ro_community: string - snmp_ro_community_desc: string - snmp_rw_community: string - snmp_rw_community_desc: string - snmp_username: string - snmp_version: string timeout: integer - username_list: list - cli_cred_len: integer + global_cli_len: integer + discovery_specific_credentials: + cli_credentials_list: + - username: string + password: string + enable_password: string + http_read_credential: + username: string + password: string + port: integer + secure: boolean + http_write_credential: + username: string + password: string + port: integer + secure: boolean + snmp_v2_read_credential: + desc: string + community: string + snmp_v2_write_credential: + desc: string + community: string + snmp_v3_credential: + username: string + snmp_mode: string + auth_password: string + auth_type: string + privacy_type: string + privacy_password: string - name: Delete disovery by name cisco.dnac.discovery_workflow_manager: @@ -361,48 +415,30 @@ def validate_input(self, state=None): discovery_spec = { 'cdp_level': {'type': 'int', 'required': False, 'default': 16}, - 'enable_password_list': {'type': 'list', 'required': False, - 'elements': 'str'}, 'start_index': {'type': 'int', 'required': False, 'default': 1}, 'records_to_return': {'type': 'int', 'required': False, 'default': 100}, - 'http_read_credential': {'type': 'dict', 'required': False}, - 'http_write_credential': {'type': 'dict', 'required': False}, + 'discovery_specific_credentials': {'type': 'dict', 'required': False}, 'ip_filter_list': {'type': 'list', 'required': False, 'elements': 'str'}, 'lldp_level': {'type': 'int', 'required': False, 'default': 16}, 'discovery_name': {'type': 'str', 'required': True}, 'netconf_port': {'type': 'str', 'required': False}, - 'password_list': {'type': 'list', 'required': False, - 'elements': 'str'}, 'preferred_mgmt_ip_method': {'type': 'str', 'required': False, 'default': 'None'}, - 'protocol_order': {'type': 'str', 'required': False}, 'retry': {'type': 'int', 'required': False}, - 'snmp_auth_passphrase': {'type': 'str', 'required': False}, - 'snmp_auth_protocol': {'type': 'str', 'required': False}, - 'snmp_mode': {'type': 'str', 'required': False}, - 'snmp_priv_passphrase': {'type': 'str', 'required': False}, - 'snmp_priv_protocol': {'type': 'str', 'required': False}, - 'snmp_ro_community': {'type': 'str', 'required': False}, - 'snmp_ro_community_desc': {'type': 'str', 'required': False}, - 'snmp_rw_community': {'type': 'str', 'required': False}, - 'snmp_rw_community_desc': {'type': 'str', 'required': False}, - 'snmp_username': {'type': 'str', 'required': False}, - 'snmp_version': {'type': 'str', 'required': False}, 'timeout': {'type': 'str', 'required': False}, - 'username_list': {'type': 'list', 'required': False, - 'elements': 'str'}, - 'cli_cred_len': {'type': 'int', 'required': False, - 'default': 1} + 'global_cli_len': {'type': 'int', 'required': False, + 'default': 1} } if state == "merged": discovery_spec["ip_address_list"] = {'type': 'list', 'required': True, 'elements': 'str'} discovery_spec["discovery_type"] = {'type': 'str', 'required': True} + discovery_spec["protocol_order"] = {'type': 'str', 'required': True} elif state == "deleted": if self.config[0].get("delete_all") is True: @@ -463,7 +499,7 @@ def get_ccc_global_credentials_v2_info(self): response = response.get('response') self.log("The Global credentials response from 'get all global credentials v2' API is {0}".format(str(response)), "DEBUG") - cli_len_inp = self.validated_config[0].get("cli_cred_len") + cli_len_inp = self.validated_config[0].get("global_cli_len") if response.get("cliCredential") is None: msg = 'Not found any CLI credentials to perform discovery' self.log(msg, "CRITICAL") @@ -580,105 +616,179 @@ def preprocess_device_discovery_handle_error(self): self.log("IP Address list's length is longer than 1", "ERROR") self.module.fail_json(msg="IP Address list's length is longer than 1", response=[]) - def http_cred_failure(self, msg=None): + def discovery_specific_cred_failure(self, msg=None): """ Method for failing discovery if there is any discrepancy in the http credentials passed by the user """ + self.log(msg, "CRITICAL") self.module.fail_json(msg=msg) - def create_params(self, credential_ids=None, ip_address_list=None): + def handle_discovery_specific_credentials(self, new_object_params=None): """ - Create a new parameter object based on the validated configuration, - credential IDs, and IP address list. + Method to convert values for create_params API when discovery specific paramters + are passed as input. Parameters: - - credential_ids: The list of credential IDs to include in the - parameters. If not provided, an empty list is used. - - ip_address_list: The list of IP addresses to include in the - parameters. If not provided, None is used. + - new_object_params: The dictionary storing various parameters for calling the + start discovery API Returns: - - new_object_params: A dictionary containing the newly created - parameters. + - new_object_params: The dictionary storing various parameters for calling the + start discovery API in an updated fashion """ - if credential_ids is None: - credential_ids = [] + discovery_specific_credentials = self.validated_config[0].get('discovery_specific_credentials') + cli_credentials_list = discovery_specific_credentials.get('cli_credentials_list') + http_read_credential = discovery_specific_credentials.get('http_read_credential') + http_write_credential = discovery_specific_credentials.get('http_write_credential') + snmp_v2_read_credential = discovery_specific_credentials.get('snmp_v2_read_credential') + snmp_v2_write_credential = discovery_specific_credentials.get('snmp_v2_write_credential') + snmp_v3_credential = discovery_specific_credentials.get('snmp_v3_credential') + net_conf_port = discovery_specific_credentials.get('net_conf_port') + + if cli_credentials_list: + if not isinstance(cli_credentials_list, list): + msg = "Device Specific ClI credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(cli_credentials_list) > 0: + username_list = [] + password_list = [] + enable_password_list = [] + for cli_cred in cli_credentials_list: + if cli_cred.get('username') and cli_cred.get('password') and cli_cred.get('enable_password'): + username_list.append(cli_cred.get('username')) + password_list.append(cli_cred.get('password')) + enable_password_list.append(cli_cred.get('enable_password')) + else: + msg = "username, password and enable_password must be passed toether for creating CLI credentials" + self.discovery_specific_cred_failure(msg=msg) + new_object_params['userNameList'] = username_list + new_object_params['passwordList'] = password_list + new_object_params['enablePasswordList'] = enable_password_list - http_read_credential = self.validated_config[0].get('http_read_credential') - http_write_credential = self.validated_config[0].get('http_write_credential') if http_read_credential: if not (http_read_credential.get('password') and isinstance(http_read_credential.get('password'), str)): msg = "The password for the HTTP read credential must be of string type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not (http_read_credential.get('username') and isinstance(http_read_credential.get('username'), str)): msg = "The username for the HTTP read credential must be of string type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not (http_read_credential.get('port') and isinstance(http_read_credential.get('port'), int)): msg = "The port for the HTTP read Credential must be of integer type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not isinstance(http_read_credential.get('secure'), bool): msg = "Secure for HTTP read Credential must be of type boolean." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) + new_object_params['httpReadCredential'] = http_read_credential if http_write_credential: if not (http_write_credential.get('password') and isinstance(http_write_credential.get('password'), str)): msg = "The password for the HTTP write credential must be of string type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not (http_write_credential.get('username') and isinstance(http_write_credential.get('username'), str)): msg = "The username for the HTTP write credential must be of string type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not (http_write_credential.get('port') and isinstance(http_write_credential.get('port'), int)): msg = "The port for the HTTP write Credential must be of integer type." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) if not isinstance(http_write_credential.get('secure'), bool): msg = "Secure for HTTP write Credential must be of type boolean." - self.http_cred_failure(msg=msg) + self.discovery_specific_cred_failure(msg=msg) + new_object_params['httpWriteCredential'] = http_write_credential + + if snmp_v2_read_credential: + if not (snmp_v2_read_credential.get('desc')) and isinstance(snmp_v2_read_credential.get('desc'), str): + msg = "Name/description for the SNMP v2 read credential must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v2_read_credential.get('community')) and isinstance(snmp_v2_read_credential.get('community'), str): + msg = "The community string must be of string type" + self.discovery_specific_cred_failure(msg=msg) + new_object_params['snmpROCommunityDesc'] = snmp_v2_read_credential.get('desc') + new_object_params['snmpROCommunity'] = snmp_v2_read_credential.get('community') + new_object_params['snmpVersion'] = "v2" + + if snmp_v2_write_credential: + if not (snmp_v2_write_credential.get('desc')) and isinstance(snmp_v2_write_credential.get('desc'), str): + msg = "Name/description for the SNMP v2 write credential must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v2_write_credential.get('community')) and isinstance(snmp_v2_write_credential.get('community'), str): + msg = "The community string must be of string type" + self.discovery_specific_cred_failure(msg=msg) + new_object_params['snmpRWCommunityDesc'] = snmp_v2_write_credential.get('desc') + new_object_params['snmpRWCommunity'] = snmp_v2_write_credential.get('community') + new_object_params['snmpVersion'] = "v2" + + if snmp_v3_credential: + if not (snmp_v3_credential.get('username')) and isinstance(snmp_v3_credential.get('username'), str): + msg = "Username of SNMP v3 protocol must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v3_credential.get('snmp_mode')) and isinstance(snmp_v3_credential.get('snmp_mode'), str): + msg = "Mode of SNMP is madantory to use SNMPv3 protocol and must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if (snmp_v3_credential.get('snmp_mode')) == "AUTHPRIV" or snmp_v3_credential.get('snmp_mode') == "AUTHNOPRIV": + if not (snmp_v3_credential.get('auth_password')) and isinstance(snmp_v3_credential.get('auth_password'), str): + msg = "Authorization password must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v3_credential.get('auth_type')) and isinstance(snmp_v3_credential.get('auth_type'), str): + msg = "Authorization type must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if snmp_v3_credential.get('snmp_mode') == "AUTHPRIV": + if not (snmp_v3_credential.get('privacy_type')) and isinstance(snmp_v3_credential.get('privacy_type'), str): + msg = "Privacy type must be of string type" + self.discovery_specific_cred_failure(msg=msg) + if not (snmp_v3_credential.get('privacy_password')) and isinstance(snmp_v3_credential.get('privacy_password'), str): + msg = "Privacy password must be of string type" + self.discovery_specific_cred_failure(msg=msg) + new_object_params['snmpUserName'] = snmp_v3_credential.get('username') + new_object_params['snmpMode'] = snmp_v3_credential.get('snmp_mode') + new_object_params['snmpAuthPassphrase'] = snmp_v3_credential.get('auth_password') + new_object_params['snmpAuthProtocol'] = snmp_v3_credential.get('auth_type') + new_object_params['snmpPrivProtocol'] = snmp_v3_credential.get('privacy_type') + new_object_params['snmpPrivPassphrase'] = snmp_v3_credential.get('privacy_password') + new_object_params['snmpVersion'] = "v3" + + if net_conf_port: + new_object_params['netconfPort'] = str(net_conf_port) + + return new_object_params + + def create_params(self, credential_ids=None, ip_address_list=None): + """ + Create a new parameter object based on the validated configuration, + credential IDs, and IP address list. + + Parameters: + - credential_ids: The list of credential IDs to include in the + parameters. If not provided, an empty list is used. + - ip_address_list: The list of IP addresses to include in the + parameters. If not provided, None is used. + + Returns: + - new_object_params: A dictionary containing the newly created + parameters. + """ + + if credential_ids is None: + credential_ids = [] new_object_params = {} new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') new_object_params['discoveryType'] = self.validated_config[0].get('discovery_type') - new_object_params['enablePasswordList'] = self.validated_config[0].get( - 'enable_password_list') new_object_params['globalCredentialIdList'] = credential_ids - new_object_params['httpReadCredential'] = self.validated_config[0].get( - 'http_read_credential') - new_object_params['httpWriteCredential'] = self.validated_config[0].get( - 'http_write_credential') new_object_params['ipAddressList'] = ip_address_list new_object_params['ipFilterList'] = self.validated_config[0].get('ip_filter_list') new_object_params['lldpLevel'] = self.validated_config[0].get('lldp_level') new_object_params['name'] = self.validated_config[0].get('discovery_name') - new_object_params['netconfPort'] = self.validated_config[0].get('netconf_port') - new_object_params['passwordList'] = self.validated_config[0].get('password_list') - new_object_params['preferredMgmtIPMethod'] = self.validated_config[0].get( - 'preferred_mgmt_ip_method') + new_object_params['preferredMgmtIPMethod'] = self.validated_config[0].get('preferred_mgmt_ip_method') new_object_params['protocolOrder'] = self.validated_config[0].get('protocol_order') new_object_params['retry'] = self.validated_config[0].get('retry') - new_object_params['snmpAuthPassphrase'] = self.validated_config[0].get( - 'snmp_auth_Passphrase') - new_object_params['snmpAuthProtocol'] = self.validated_config[0].get( - 'snmp_auth_protocol') - new_object_params['snmpMode'] = self.validated_config[0].get('snmp_mode') - new_object_params['snmpPrivPassphrase'] = self.validated_config[0].get( - 'snmp_priv_passphrase') - new_object_params['snmpPrivProtocol'] = self.validated_config[0].get( - 'snmp_priv_protocol') - new_object_params['snmpROCommunity'] = self.validated_config[0].get( - 'snmp_ro_community') - new_object_params['snmpROCommunityDesc'] = self.validated_config[0].get( - 'snmp_ro_community_desc') - new_object_params['snmpRWCommunity'] = self.validated_config[0].get( - 'snmp_rw_community') - new_object_params['snmpRWCommunityDesc'] = self.validated_config[0].get( - 'snmp_rw_community_desc') - new_object_params['snmpUserName'] = self.validated_config[0].get( - 'snmp_username') - new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') new_object_params['timeout'] = self.validated_config[0].get('timeout') - new_object_params['userNameList'] = self.validated_config[0].get('username_list') + + if self.validated_config[0].get('discovery_specific_credentials'): + self.handle_discovery_specific_credentials(new_object_params=new_object_params) + self.log("The payload/object created for calling the start discovery API is {0}".format(str(new_object_params)), "INFO") return new_object_params @@ -1085,6 +1195,19 @@ def verify_diff_deleted(self, config): self.log("Current State (have): {0}".format(str(self.have)), "INFO") self.log("Desired State (want): {0}".format(str(config)), "INFO") # Code to validate Cisco Catalyst Center config for deleted state + if config.get("delete_all") is True: + count_discoveries = self.dnac_apply['exec']( + family="discovery", + function="get_count_of_all_discovery_jobs", + ) + if count_discoveries == 0: + self.log("All discoveries are deleted", "INFO") + + else: + self.log("All discoveries are not deleted", "WARNING") + self.status = "success" + return self + discovery_task_info = self.lookup_discovery_by_range_via_name() discovery_name = config.get('discovery_name') if discovery_task_info: From 76380c477b3ab30d01e3e2fff204eb994c389fb1 Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 15 Feb 2024 17:12:04 +0000 Subject: [PATCH 38/64] Making changes to incorporate discovery specific credentials --- plugins/modules/discovery_intent.py | 8 ++++---- plugins/modules/discovery_workflow_manager.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 0a55ab1cc2..fbfb50569b 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -126,10 +126,10 @@ type: dict suboptions: desc: - descritption: Name/Description of the SNMP read credential to be used for creation of snmp_v2_read_credential. + description: Name/Description of the SNMP read credential to be used for creation of snmp_v2_read_credential. type: str community: - descritption: The read-only community string enables Cisco Catalyst Center to extract read-only data from device. + description: The read-only community string enables Cisco Catalyst Center to extract read-only data from device. type: str snmp_v2_write_credential: description: @@ -138,10 +138,10 @@ type: dict suboptions: desc: - descritption: Name/Description of the SNMP write credential to be used for creation of snmp_v2_write_credential. + description: Name/Description of the SNMP write credential to be used for creation of snmp_v2_write_credential. type: str community: - descritption: The read-write community string is used to extract data and alter device configurations. + description: The read-write community string is used to extract data and alter device configurations. type: str snmp_v3_credential: description: diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 7100bbb60b..d995a13cdb 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -126,10 +126,10 @@ type: dict suboptions: desc: - descritption: Name/Description of the SNMP read credential to be used for creation of snmp_v2_read_credential. + description: Name/Description of the SNMP read credential to be used for creation of snmp_v2_read_credential. type: str community: - descritption: The read-only community string enables Cisco Catalyst Center to extract read-only data from device. + description: The read-only community string enables Cisco Catalyst Center to extract read-only data from device. type: str snmp_v2_write_credential: description: @@ -138,10 +138,10 @@ type: dict suboptions: desc: - descritption: Name/Description of the SNMP write credential to be used for creation of snmp_v2_write_credential. + description: Name/Description of the SNMP write credential to be used for creation of snmp_v2_write_credential. type: str community: - descritption: The read-write community string is used to extract data and alter device configurations. + description: The read-write community string is used to extract data and alter device configurations. type: str snmp_v3_credential: description: From 281dd303a25200cc74b99331dce0c71babdddf5e Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 16 Feb 2024 05:31:27 +0000 Subject: [PATCH 39/64] Making changes to incorporate discovery specific credentials --- plugins/modules/discovery_intent.py | 60 +++++++++++-------- plugins/modules/discovery_workflow_manager.py | 60 +++++++++++-------- 2 files changed, 72 insertions(+), 48 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index fbfb50569b..b9111b00c4 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -49,7 +49,14 @@ elements: str required: true discovery_type: - description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) + description: + - Determines the type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) + - SINGLE type discovery discovers a single device with single IP address. + - RANGE type discovery discovers multiple devices falling in the single IP address range. + - MULTI RANGE type discovery discovers multiple devices falling in multiple IP address ranges. + - CDP uses Cisco Discovery Protocol to discover all the devices in the subsequent layers of the IP address passed. + - LLDP uses Link Layer Discovery Protocol to discover all the devices in the subsequent layers of the IP address passed. + - CIDR uses Classless Inter-Domain Routing to discover devices based on subnet filtering. type: str required: true choices: [ 'SINGLE', 'RANGE', 'MULTI RANGE', 'CDP', 'LLDP', 'CIDR'] @@ -70,54 +77,56 @@ type: int default: 100 discovery_specific_credentials: - description: Credentials specifically created by the user while performing discovery. + description: Credentials specifically created by the user for performing device discovery. type: dict suboptions: cli_credentials_list: - description: List of CLI credentials to be used while performing discovery. + description: List of CLI credentials to be used during device discovery. type: list elements: dict suboptions: username: - description: The username for CLI authentication, which is mandatory when using CLI credential. + description: Username for CLI authentication, mandatory when using CLI credentials. type: str password: - description: The password for CLI authentication, which is mandatory when using CLI credential. + description: Password for CLI authentication, mandatory when using CLI credential. type: str enable_password: - description: The enable password for CLI authentication, which is mandatory when using CLI credential. + description: Enable password for CLI authentication, mandatory when using CLI credential. type: str http_read_credential: - description: HTTP read credentials for hosting a device + description: HTTP read credential is used for authentication purposes and specifically utilized to + grant read-only access to certain resources from the device. type: dict suboptions: username: - description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + description: Username for HTTP(S) Read authentication, mandatory when using HTTP credentials. type: str password: - description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + description: Password for HTTP(S) Read authentication, mandatory when using HTTP credentials. type: str port: - description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + description: Port for HTTP(S) Read authentication, mandatory for using HTTP credentials. type: int secure: - description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + description: Flag for HTTP(S) Read authentication, not mandatory when using HTTP credentials. type: bool http_write_credential: - description: HTTP write credentials for hosting a device + description: HTTP write credential is used for authentication purposes and grants Cisco Catalyst Center the + ability to alter configurations, update software, or perform other modifications on a network device. type: dict suboptions: username: - description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + description: Username for HTTP(S) Write authentication, mandatory when using HTTP credentials. type: str password: - description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + description: Password for HTTP(S) Write authentication, mandatory when using HTTP credentials. type: str port: - description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + description: Port for HTTP(S) Write authentication, mandatory when using HTTP credentials. type: int secure: - description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + description: Flag for HTTP(S) Write authentication, not mandatory when using HTTP credentials. type: bool snmp_v2_read_credential: description: @@ -129,7 +138,7 @@ description: Name/Description of the SNMP read credential to be used for creation of snmp_v2_read_credential. type: str community: - description: The read-only community string enables Cisco Catalyst Center to extract read-only data from device. + description: SNMP V2 Read community string enables Cisco Catalyst Center to extract read-only data from device. type: str snmp_v2_write_credential: description: @@ -141,7 +150,7 @@ description: Name/Description of the SNMP write credential to be used for creation of snmp_v2_write_credential. type: str community: - description: The read-write community string is used to extract data and alter device configurations. + description: SNMP V2 Write community string is used to extract data and alter device configurations. type: str snmp_v3_credential: description: @@ -153,12 +162,16 @@ description: Username of the SNMP v3 protocol to be used. type: str snmp_mode: - description: Mode of SNMP which determines the encryption level of our community string. + description: + - Mode of SNMP which determines the encryption level of our community string. + - AUTHPRIV mode uses both Authentication and Encryption. + - AUTHNOPRIV mode uses Authentication but no Encryption. + - NOAUTHNOPRIV mode doesn’t use either Authentication or Encryption. type: str choices: [ 'AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV' ] auth_password: description: - - Authentiaction Password of the SNMP v3 protocol to be used. + - Authentication Password of the SNMP v3 protocol to be used. - Must be of length greater than 7 characters. - Not required for NOAUTHNOPRIV snmp_mode. type: str @@ -173,20 +186,20 @@ privacy_type: description: - Privacy type/protocol of the SNMP v3 protocol to be used in AUTHPRIV SNMP mode - - Not required for 'AUTHNOPRIV' and NOAUTHNOPRIV snmp_mode. + - Not required for AUTHNOPRIV and NOAUTHNOPRIV snmp_mode. type: str choices: [ 'AES128', 'AES192', 'AES256' ] privacy_password: description: - Privacy password of the SNMP v3 protocol to be used in AUTHPRIV SNMP mode - - Not required for 'AUTHNOPRIV' and NOAUTHNOPRIV snmp_mode. + - Not required for AUTHNOPRIV and NOAUTHNOPRIV snmp_mode. type: str net_conf_port: description: - To be used when network contains IOS XE-based wireless controllers. - This is used for discovery and the enabling of wireless services on the controllers. - Requires valid SSH credentials to work. - - Standard ports like 22, 80, 8080 must be avoided. + - Avoid standard ports like 22, 80, and 8080. type: str ip_filter_list: description: List of IP adddrsess that needs to get filtered out from the IP addresses passed. @@ -1202,7 +1215,6 @@ def verify_diff_deleted(self, config): ) if count_discoveries == 0: self.log("All discoveries are deleted", "INFO") - else: self.log("All discoveries are not deleted", "WARNING") self.status = "success" diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index d995a13cdb..c5de705da4 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -49,7 +49,14 @@ elements: str required: true discovery_type: - description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) + description: + - Determines the type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) + - SINGLE type discovery discovers a single device with single IP address. + - RANGE type discovery discovers multiple devices falling in the single IP address range. + - MULTI RANGE type discovery discovers multiple devices falling in multiple IP address ranges. + - CDP uses Cisco Discovery Protocol to discover all the devices in the subsequent layers of the IP address passed. + - LLDP uses Link Layer Discovery Protocol to discover all the devices in the subsequent layers of the IP address passed. + - CIDR uses Classless Inter-Domain Routing to discover devices based on subnet filtering. type: str required: true choices: [ 'SINGLE', 'RANGE', 'MULTI RANGE', 'CDP', 'LLDP', 'CIDR'] @@ -70,54 +77,56 @@ type: int default: 100 discovery_specific_credentials: - description: Credentials specifically created by the user while performing discovery. + description: Credentials specifically created by the user for performing device discovery. type: dict suboptions: cli_credentials_list: - description: List of CLI credentials to be used while performing discovery. + description: List of CLI credentials to be used during device discovery. type: list elements: dict suboptions: username: - description: The username for CLI authentication, which is mandatory when using CLI credential. + description: Username for CLI authentication, mandatory when using CLI credentials. type: str password: - description: The password for CLI authentication, which is mandatory when using CLI credential. + description: Password for CLI authentication, mandatory when using CLI credential. type: str enable_password: - description: The enable password for CLI authentication, which is mandatory when using CLI credential. + description: Enable password for CLI authentication, mandatory when using CLI credential. type: str http_read_credential: - description: HTTP read credentials for hosting a device + description: HTTP read credential is used for authentication purposes and specifically utilized to + grant read-only access to certain resources from the device. type: dict suboptions: username: - description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + description: Username for HTTP(S) Read authentication, mandatory when using HTTP credentials. type: str password: - description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + description: Password for HTTP(S) Read authentication, mandatory when using HTTP credentials. type: str port: - description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + description: Port for HTTP(S) Read authentication, mandatory for using HTTP credentials. type: int secure: - description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + description: Flag for HTTP(S) Read authentication, not mandatory when using HTTP credentials. type: bool http_write_credential: - description: HTTP write credentials for hosting a device + description: HTTP write credential is used for authentication purposes and grants Cisco Catalyst Center the + ability to alter configurations, update software, or perform other modifications on a network device. type: dict suboptions: username: - description: The username for HTTP(S) authentication, which is mandatory when using HTTP credentials. + description: Username for HTTP(S) Write authentication, mandatory when using HTTP credentials. type: str password: - description: The password for HTTP(S) authentication. Mandatory for utilizing HTTP credentials. + description: Password for HTTP(S) Write authentication, mandatory when using HTTP credentials. type: str port: - description: The HTTP(S) port number, which is mandatory for using HTTP credentials. + description: Port for HTTP(S) Write authentication, mandatory when using HTTP credentials. type: int secure: - description: This is a flag for HTTP(S). Its usage is not mandatory for HTTP credentials. + description: Flag for HTTP(S) Write authentication, not mandatory when using HTTP credentials. type: bool snmp_v2_read_credential: description: @@ -129,7 +138,7 @@ description: Name/Description of the SNMP read credential to be used for creation of snmp_v2_read_credential. type: str community: - description: The read-only community string enables Cisco Catalyst Center to extract read-only data from device. + description: SNMP V2 Read community string enables Cisco Catalyst Center to extract read-only data from device. type: str snmp_v2_write_credential: description: @@ -141,7 +150,7 @@ description: Name/Description of the SNMP write credential to be used for creation of snmp_v2_write_credential. type: str community: - description: The read-write community string is used to extract data and alter device configurations. + description: SNMP V2 Write community string is used to extract data and alter device configurations. type: str snmp_v3_credential: description: @@ -153,12 +162,16 @@ description: Username of the SNMP v3 protocol to be used. type: str snmp_mode: - description: Mode of SNMP which determines the encryption level of our community string. + description: + - Mode of SNMP which determines the encryption level of our community string. + - AUTHPRIV mode uses both Authentication and Encryption. + - AUTHNOPRIV mode uses Authentication but no Encryption. + - NOAUTHNOPRIV mode doesn’t use either Authentication or Encryption. type: str choices: [ 'AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV' ] auth_password: description: - - Authentiaction Password of the SNMP v3 protocol to be used. + - Authentication Password of the SNMP v3 protocol to be used. - Must be of length greater than 7 characters. - Not required for NOAUTHNOPRIV snmp_mode. type: str @@ -173,20 +186,20 @@ privacy_type: description: - Privacy type/protocol of the SNMP v3 protocol to be used in AUTHPRIV SNMP mode - - Not required for 'AUTHNOPRIV' and NOAUTHNOPRIV snmp_mode. + - Not required for AUTHNOPRIV and NOAUTHNOPRIV snmp_mode. type: str choices: [ 'AES128', 'AES192', 'AES256' ] privacy_password: description: - Privacy password of the SNMP v3 protocol to be used in AUTHPRIV SNMP mode - - Not required for 'AUTHNOPRIV' and NOAUTHNOPRIV snmp_mode. + - Not required for AUTHNOPRIV and NOAUTHNOPRIV snmp_mode. type: str net_conf_port: description: - To be used when network contains IOS XE-based wireless controllers. - This is used for discovery and the enabling of wireless services on the controllers. - Requires valid SSH credentials to work. - - Standard ports like 22, 80, 8080 must be avoided. + - Avoid standard ports like 22, 80, and 8080. type: str ip_filter_list: description: List of IP adddrsess that needs to get filtered out from the IP addresses passed. @@ -1202,7 +1215,6 @@ def verify_diff_deleted(self, config): ) if count_discoveries == 0: self.log("All discoveries are deleted", "INFO") - else: self.log("All discoveries are not deleted", "WARNING") self.status = "success" From b45c5dbf5fd719d52d70be31bdbe50dc847378d4 Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 16 Feb 2024 05:34:57 +0000 Subject: [PATCH 40/64] Making changes to incorporate discovery specific credentials --- plugins/modules/discovery_intent.py | 2 +- plugins/modules/discovery_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index b9111b00c4..15c4df096d 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -166,7 +166,7 @@ - Mode of SNMP which determines the encryption level of our community string. - AUTHPRIV mode uses both Authentication and Encryption. - AUTHNOPRIV mode uses Authentication but no Encryption. - - NOAUTHNOPRIV mode doesn’t use either Authentication or Encryption. + - NOAUTHNOPRIV mode does not use either Authentication or Encryption. type: str choices: [ 'AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV' ] auth_password: diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index c5de705da4..840048b5ab 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -166,7 +166,7 @@ - Mode of SNMP which determines the encryption level of our community string. - AUTHPRIV mode uses both Authentication and Encryption. - AUTHNOPRIV mode uses Authentication but no Encryption. - - NOAUTHNOPRIV mode doesn’t use either Authentication or Encryption. + - NOAUTHNOPRIV mode does not use either Authentication or Encryption. type: str choices: [ 'AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV' ] auth_password: From a2f7c7930b004a0e0ed2c0641e823cc84a7af6ea Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 16 Feb 2024 06:44:14 +0000 Subject: [PATCH 41/64] Making changes to incorporate discovery specific credentials --- plugins/modules/discovery_intent.py | 21 +++++++++---------- plugins/modules/discovery_workflow_manager.py | 21 +++++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 15c4df096d..ad2271e9cb 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -11,9 +11,9 @@ DOCUMENTATION = r""" --- module: discovery_intent -short_description: Resource module for discovery related functions +short_description: A resource module for handling device discovery tasks. description: -- Manage operations disicover devices using IP address/range, CDP, LLDP and delete discoveries +- Manages device discovery using IP address, address range, CDP, and LLDP, including deletion of discovered devices. - API to discover a device or multiple devices - API to delete a discovery of a device or multiple devices version_added: '6.6.0' @@ -49,14 +49,13 @@ elements: str required: true discovery_type: - description: - - Determines the type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) - - SINGLE type discovery discovers a single device with single IP address. - - RANGE type discovery discovers multiple devices falling in the single IP address range. - - MULTI RANGE type discovery discovers multiple devices falling in multiple IP address ranges. - - CDP uses Cisco Discovery Protocol to discover all the devices in the subsequent layers of the IP address passed. - - LLDP uses Link Layer Discovery Protocol to discover all the devices in the subsequent layers of the IP address passed. - - CIDR uses Classless Inter-Domain Routing to discover devices based on subnet filtering. + description: Determines the method of device discovery. Here are the available options. + - SINGLE discovers a single device using a single IP address. + - RANGE discovers multiple devices within a single IP address range. + - MULTI RANGE discovers devices across multiple IP address ranges. + - CDP uses Cisco Discovery Protocol to discover devices in subsequent layers of the given IP address. + - LLDP uses Link Layer Discovery Protocol to discover devices in subsequent layers of the specified IP address. + - CIDR discovers devices based on subnet filtering using Classless Inter-Domain Routing. type: str required: true choices: [ 'SINGLE', 'RANGE', 'MULTI RANGE', 'CDP', 'LLDP', 'CIDR'] @@ -206,7 +205,7 @@ type: list elements: str discovery_name: - description: Name of the discovery taski + description: Name of the discovery task type: str required: true preferred_mgmt_ip_method: diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 840048b5ab..86bc2d66c6 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -11,9 +11,9 @@ DOCUMENTATION = r""" --- module: discovery_workflow_manager -short_description: Resource module for discovery related functions +short_description: A resource module for handling device discovery tasks. description: -- Manage operations disicover devices using IP address/range, CDP, LLDP and delete discoveries +- Manages device discovery using IP address, address range, CDP, and LLDP, including deletion of discovered devices. - API to discover a device or multiple devices - API to delete a discovery of a device or multiple devices version_added: '6.6.0' @@ -49,14 +49,13 @@ elements: str required: true discovery_type: - description: - - Determines the type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) - - SINGLE type discovery discovers a single device with single IP address. - - RANGE type discovery discovers multiple devices falling in the single IP address range. - - MULTI RANGE type discovery discovers multiple devices falling in multiple IP address ranges. - - CDP uses Cisco Discovery Protocol to discover all the devices in the subsequent layers of the IP address passed. - - LLDP uses Link Layer Discovery Protocol to discover all the devices in the subsequent layers of the IP address passed. - - CIDR uses Classless Inter-Domain Routing to discover devices based on subnet filtering. + description: Determines the method of device discovery. Here are the available options. + - SINGLE discovers a single device using a single IP address. + - RANGE discovers multiple devices within a single IP address range. + - MULTI RANGE discovers devices across multiple IP address ranges. + - CDP uses Cisco Discovery Protocol to discover devices in subsequent layers of the given IP address. + - LLDP uses Link Layer Discovery Protocol to discover devices in subsequent layers of the specified IP address. + - CIDR discovers devices based on subnet filtering using Classless Inter-Domain Routing. type: str required: true choices: [ 'SINGLE', 'RANGE', 'MULTI RANGE', 'CDP', 'LLDP', 'CIDR'] @@ -206,7 +205,7 @@ type: list elements: str discovery_name: - description: Name of the discovery taski + description: Name of the discovery task type: str required: true preferred_mgmt_ip_method: From ed8a74b9648f4af0e64d265c69c39fa58b42af93 Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 16 Feb 2024 06:48:48 +0000 Subject: [PATCH 42/64] Making changes to incorporate discovery specific credentials --- plugins/modules/discovery_intent.py | 5 +++-- plugins/modules/discovery_workflow_manager.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index ad2271e9cb..315d54c3b5 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -213,8 +213,9 @@ type: str default: None protocol_order: - description: Order of protocol (ssh/telnet) in which device connection will be tried. For example, 'telnet' - only telnet - 'ssh, - telnet' - ssh with higher order than telnet + Description: Determines the order in which device connections will be attempted. Here are the options + - "telnet" Only telnet connections will be tried. + - "ssh, telnet" SSH (Secure Shell) will be attempted first, followed by telnet if SSH fails. type: str required: true retry: diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 86bc2d66c6..ed743b031c 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -213,8 +213,9 @@ type: str default: None protocol_order: - description: Order of protocol (ssh/telnet) in which device connection will be tried. For example, 'telnet' - only telnet - 'ssh, - telnet' - ssh with higher order than telnet + Description: Determines the order in which device connections will be attempted. Here are the options + - "telnet" Only telnet connections will be tried. + - "ssh, telnet" SSH (Secure Shell) will be attempted first, followed by telnet if SSH fails. type: str required: true retry: From 5c2fcc838d2c5503220cab39326ca4c1be54e4ae Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 16 Feb 2024 14:43:30 +0530 Subject: [PATCH 43/64] Change type to site_type and write one API to convert it, also chnaged some parameters types from int to float and mention examples as well. --- playbooks/site_workflow_manager.yml | 10 +-- plugins/module_utils/dnac.py | 27 ++++++++ plugins/modules/site_intent.py | 82 ++++++++++++----------- plugins/modules/site_workflow_manager.py | 83 +++++++++++++----------- 4 files changed, 122 insertions(+), 80 deletions(-) diff --git a/playbooks/site_workflow_manager.yml b/playbooks/site_workflow_manager.yml index 939e8e7f99..79b42a436c 100644 --- a/playbooks/site_workflow_manager.yml +++ b/playbooks/site_workflow_manager.yml @@ -22,12 +22,12 @@ floor: name: Test_Floor6 parent_name: 'Global/USA/San Francisco/BGL_18' - length: "103" - width: "75" - height: "50" + length: 103.23 + width: 75.1 + height: 50.22 rf_model: 'Cubes And Walled Offices' floor_number: 3 - type: floor + site_type: floor - site: area: name: Abc @@ -36,4 +36,4 @@ latitude: 22.2111 longitude: -42.1234434 country: "United States" - type: area + site_type: area diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 7229de0421..a12e7eaf47 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -458,6 +458,33 @@ def camel_to_snake_case(self, config): return config return new_config + def update_site_type_key(self, config): + """ + Replace 'site_type' key with 'type' in the config. + + Parameters: + config (list or dict) - Configuration details. + + Returns: + updated_config (list or dict) - Updated config after replacing the keys. + """ + + if isinstance(config, dict): + new_config = {} + for key, value in config.items(): + if key == "site_type": + new_key = "type" + else: + new_key = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower() + new_value = self.update_site_type_key(value) + new_config[new_key] = new_value + elif isinstance(config, list): + return [self.update_site_type_key(item) for item in config] + else: + return config + + return new_config + def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 578e4cde4e..5fefe8589b 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -40,7 +40,7 @@ elements: dict required: True suboptions: - type: + site_type: description: Type of site to create/update/delete (eg area, building, floor). type: str site: @@ -48,53 +48,59 @@ type: dict suboptions: area: - description: Contains details for creating or managing an area within a site. + description: Configuration details for creating or managing an area within a site. type: dict suboptions: name: - description: Name of the area (eg Area1). + description: Name of the area to be created or managed (e.g., "Area1"). type: str parent_name: - description: Complete Parent name of the Area to be created/deleted(eg Global/USA). + description: The full name of the parent under which the area will be created/managed/deleted (e.g., "Global/USA"). type: str building: - description: Contains details for creating or managing a building within a site. + description: Configuration details required for creating or managing a building within a site. type: dict suboptions: address: - description: Address of the building to be created. + description: Physical address of the building that is to be created or managed. type: str latitude: - description: Latitude coordinate of the building (eg 37.338). Values between -90 to +90. + description: Geographical latitude coordinate of the building. For example, use 37.338 for a location in San Jose, California. + Valid values range from -90.0 to +90.0 degrees. + type: float longitude: - description: Longitude coordinate of the building (eg -121.832). Values between -180 to +180. - type: int + description: Geographical longitude coordinate of the building. For example, use -121.832 for a location in San Jose, California. + Valid values range from -180.0 to +180.0 degrees. + type: float name: - description: Name of the building (eg building1). + description: Name of the building (e.g., "Building1"). type: str parent_name: - description: Complete parent name of the building to be created/deleted(eg Global/USA/San Francisco). + description: Hierarchical parent path of the building, indicating its location within the site (e.g., "Global/USA/San Francisco"). type: str floor: - description: Contains details for creating or managing a floor within a site. + description: Configuration details required for creating or managing a floor within a site. type: dict suboptions: height: - description: Height of the floor units is ft. (eg 15). - type: int + description: Height of the floor in feet (e.g., 15.23). + type: float length: - description: Length of the floor units is ft. (eg 100). - type: int + description: Length of the floor in feet (e.g., 100.11). + type: float name: - description: Name of the floor (eg floor-1). + description: Name of the floor (e.g., "Floor-1"). type: str parent_name: - description: Complete parent name of the floor to be created(eg Global/USA/San Francisco/BGL_18). + description: Hierarchical parent path of the floor, indicating its location within the site (e.g., + "Global/USA/San Francisco/BGL_18"). type: str rf_model: - description: Type of floor (allowed values are 'Cubes And Walled Offices', 'Drywall Office Only', 'Indoor High Ceiling', - 'Outdoor Open Space'). It refers to the Radio Frequency (RF) model of the floor. It is essential in wireless - networking to simulate and optimize radio signal propagation and coverage within a physical space. + description: The RF (Radio Frequency) model type for the floor, which is essential for simulating and optimizing wireless + network coverage. Select from the following allowed values, which describe different environmental signal propagation + characteristics. + Type of floor (allowed values are 'Cubes And Walled Offices', 'Drywall Office Only', 'Indoor High Ceiling', + 'Outdoor Open Space'). Cubes And Walled Offices - This RF model typically represents indoor areas with cubicles or walled offices, where radio signals may experience attenuation due to walls and obstacles. Drywall Office Only - This RF model indicates an environment with drywall partitions, commonly found in office spaces, @@ -105,10 +111,11 @@ and may follow different patterns compared to indoor environments. type: str width: - description: Width of the floor units is ft. (eg 100). - type: int + description: Width of the floor in feet (e.g., 100.22). + type: float floor_number: - description: Floor number in the building/site (eg 5) can be given only while creating the floor site. + description: Floor number within the building site (e.g., 5). This value can only be specified during the creation of the + floor and cannot be modified afterward. type: int requirements: @@ -144,7 +151,7 @@ area: name: string parent_name: string - type: string + site_type: string - name: Create a new building site cisco.dnac.site_intent: @@ -162,11 +169,11 @@ - site: building: address: string - latitude: 0 - longitude: 0 + latitude: float + longitude: float name: string parent_name: string - type: string + site_type: string - name: Create a Floor site under the building cisco.dnac.site_intent: @@ -185,12 +192,12 @@ floor: name: string parent_name: string - length: int - width: int - height: int + length: float + width: float + height: float rf_model: string floor_number: int - type: string + site_type: string - name: Updating the Floor details under the building cisco.dnac.site_intent: @@ -209,10 +216,10 @@ floor: name: string parent_name: string - length: int - width: int - height: int - type: string + length: float + width: float + height: float + site_type: string - name: Deleting any site you need site name and parent name cisco.dnac.site_intent: @@ -231,7 +238,7 @@ floor: name: string parent_name: string - type: string + site_type: string """ RETURN = r""" @@ -366,6 +373,7 @@ def validate_input(self): site=dict(required=True, type='dict'), ) self.config = self.camel_to_snake_case(self.config) + self.config = self.update_site_type_key(self.config) # Validate site params valid_temp, invalid_params = validate_list_of_dicts( diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index dd1e41c9a4..737a2c5cc1 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -40,7 +40,7 @@ elements: dict required: True suboptions: - type: + site_type: description: Type of site to create/update/delete (eg area, building, floor). type: str site: @@ -48,54 +48,59 @@ type: dict suboptions: area: - description: Contains details for creating or managing an area within a site. + description: Configuration details for creating or managing an area within a site. type: dict suboptions: name: - description: Name of the area (eg Area1). + description: Name of the area to be created or managed (e.g., "Area1"). type: str parent_name: - description: Complete Parent name of the Area to be created/deleted(eg Global/USA). + description: The full name of the parent under which the area will be created/managed/deleted (e.g., "Global/USA"). type: str building: - description: Contains details for creating or managing a building within a site. + description: Configuration details required for creating or managing a building within a site. type: dict suboptions: address: - description: Address of the building to be created. + description: Physical address of the building that is to be created or managed. type: str latitude: - description: Latitude coordinate of the building (eg 37.338). Values between -90 to +90. - type: int + description: Geographical latitude coordinate of the building. For example, use 37.338 for a location in San Jose, California. + Valid values range from -90.0 to +90.0 degrees. + type: float longitude: - description: Longitude coordinate of the building (eg -121.832). Values between -180 to +180. - type: int + description: Geographical longitude coordinate of the building. For example, use -121.832 for a location in San Jose, California. + Valid values range from -180.0 to +180.0 degrees. + type: float name: - description: Name of the building (eg building1). + description: Name of the building (e.g., "Building1"). type: str parent_name: - description: Complete parent name of the building to be created/deleted(eg Global/USA/San Francisco). + description: Hierarchical parent path of the building, indicating its location within the site (e.g., "Global/USA/San Francisco"). type: str floor: - description: Contains details for creating or managing a floor within a site. + description: Configuration details required for creating or managing a floor within a site. type: dict suboptions: height: - description: Height of the floor units is ft. (eg 15). - type: int + description: Height of the floor in feet (e.g., 15.23). + type: float length: - description: Length of the floor units is ft. (eg 100). - type: int + description: Length of the floor in feet (e.g., 100.11). + type: float name: - description: Name of the floor (eg floor-1). + description: Name of the floor (e.g., "Floor-1"). type: str parent_name: - description: Complete parent name of the floor to be created(eg Global/USA/San Francisco/BGL_18). + description: Hierarchical parent path of the floor, indicating its location within the site (e.g., + "Global/USA/San Francisco/BGL_18"). type: str rf_model: - description: Type of floor (allowed values are 'Cubes And Walled Offices', 'Drywall Office Only', 'Indoor High Ceiling', - 'Outdoor Open Space'). It refers to the Radio Frequency (RF) model of the floor. It is essential in wireless - networking to simulate and optimize radio signal propagation and coverage within a physical space. + description: The RF (Radio Frequency) model type for the floor, which is essential for simulating and optimizing wireless + network coverage. Select from the following allowed values, which describe different environmental signal propagation + characteristics. + Type of floor (allowed values are 'Cubes And Walled Offices', 'Drywall Office Only', 'Indoor High Ceiling', + 'Outdoor Open Space'). Cubes And Walled Offices - This RF model typically represents indoor areas with cubicles or walled offices, where radio signals may experience attenuation due to walls and obstacles. Drywall Office Only - This RF model indicates an environment with drywall partitions, commonly found in office spaces, @@ -106,10 +111,11 @@ and may follow different patterns compared to indoor environments. type: str width: - description: Width of the floor units is ft. (eg 100). - type: int + description: Width of the floor in feet (e.g., 100.22). + type: float floor_number: - description: Floor number in the building/site (eg 5) can be given only while creating the floor site. + description: Floor number within the building site (e.g., 5). This value can only be specified during the creation of the + floor and cannot be modified afterward. type: int requirements: @@ -145,7 +151,7 @@ area: name: string parent_name: string - type: string + site_type: string - name: Create a new building site cisco.dnac.site_workflow_manager: @@ -163,11 +169,11 @@ - site: building: address: string - latitude: 0 - longitude: 0 + latitude: float + longitude: float name: string parent_name: string - type: string + site_type: string - name: Create a Floor site under the building cisco.dnac.site_workflow_manager: @@ -186,12 +192,12 @@ floor: name: string parent_name: string - length: int - width: int - height: int + length: float + width: float + height: float rf_model: string floor_number: int - type: string + site_type: string - name: Updating the Floor details under the building cisco.dnac.site_workflow_manager: @@ -210,10 +216,10 @@ floor: name: string parent_name: string - length: int - width: int - height: int - type: string + length: float + width: float + height: float + site_type: string - name: Deleting any site you need site name and parent name cisco.dnac.site_workflow_manager: @@ -232,7 +238,7 @@ floor: name: string parent_name: string - type: string + site_type: string """ RETURN = r""" @@ -366,6 +372,7 @@ def validate_input(self): type=dict(required=False, type='str'), site=dict(required=True, type='dict'), ) + self.config = self.update_site_type_key(self.config) # Validate site params valid_temp, invalid_params = validate_list_of_dicts( From 9550cd80176170aa71af932ae2b2af6bcc498811 Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 16 Feb 2024 09:58:55 +0000 Subject: [PATCH 44/64] Making changes to incorporate discovery specific credentials --- plugins/modules/discovery_intent.py | 2 +- plugins/modules/discovery_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 315d54c3b5..b6f9ab7ecf 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -213,7 +213,7 @@ type: str default: None protocol_order: - Description: Determines the order in which device connections will be attempted. Here are the options + description: Determines the order in which device connections will be attempted. Here are the options - "telnet" Only telnet connections will be tried. - "ssh, telnet" SSH (Secure Shell) will be attempted first, followed by telnet if SSH fails. type: str diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index ed743b031c..5b44c4fc2e 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -213,7 +213,7 @@ type: str default: None protocol_order: - Description: Determines the order in which device connections will be attempted. Here are the options + description: Determines the order in which device connections will be attempted. Here are the options - "telnet" Only telnet connections will be tried. - "ssh, telnet" SSH (Secure Shell) will be attempted first, followed by telnet if SSH fails. type: str From 112b123f1af901670aa287ba418a6934da06b09b Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 16 Feb 2024 22:07:11 +0530 Subject: [PATCH 45/64] Fix the issue of updating snmp auth passphrase and snmp private password in inventory --- plugins/modules/inventory_intent.py | 7 ++++--- plugins/modules/inventory_workflow_manager.py | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 3dfb38d79f..cc9f186f8b 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2789,11 +2789,12 @@ def get_diff_merged(self, config): mapped_key = device_key_mapping[key] if playbook_params[mapped_key] is None: - if playbook_params['snmpMode'] == "AUTHPRIV": - playbook_params['snmpAuthPassphrase'] = csv_data_dict['snmp_auth_passphrase'] - playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] playbook_params[mapped_key] = csv_data_dict[key] + if playbook_params['snmpMode'] == "AUTHPRIV": + playbook_params['snmpAuthPassphrase'] = csv_data_dict['snmp_auth_passphrase'] + playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] + if playbook_params['snmpMode'] == "NOAUTHNOPRIV": playbook_params.pop('snmpAuthPassphrase', None) playbook_params.pop('snmpPrivPassphrase', None) diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index b10d6dac8e..da9f933ed6 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -2790,11 +2790,14 @@ def get_diff_merged(self, config): mapped_key = device_key_mapping[key] if playbook_params[mapped_key] is None: - if playbook_params['snmpMode'] == "AUTHPRIV": - playbook_params['snmpAuthPassphrase'] = csv_data_dict['snmp_auth_passphrase'] - playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] playbook_params[mapped_key] = csv_data_dict[key] + if playbook_params['snmpMode'] == "AUTHPRIV": + if not playbook_params['snmpAuthPassphrase']: + playbook_params['snmpAuthPassphrase'] = csv_data_dict['snmp_auth_passphrase'] + if not playbook_params['snmpPrivPassphrase']: + playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] + if playbook_params['snmpMode'] == "NOAUTHNOPRIV": playbook_params.pop('snmpAuthPassphrase', None) playbook_params.pop('snmpPrivPassphrase', None) From c946e199b120bf404f8862ef3cc0c7c08c2ae252 Mon Sep 17 00:00:00 2001 From: Madhan Date: Sat, 17 Feb 2024 00:19:11 +0530 Subject: [PATCH 46/64] Adding workflow modules and updating the version --- changelogs/changelog.yaml | 14 ++++++++++++++ galaxy.yml | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 74f9969840..422b6ed65b 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -792,3 +792,17 @@ releases: - Introducing log levels and log file path - Updated Documentation in template intent module - Enhancements in device_credential, inventory, discovery and template intent modules. + 6.11.0: + release_date: "2024-02-17" + changes: + release_summary: Adding new workflow manager modules in Cisco Catalyst Center + major_changes: + - The 'site_workflow_manager' module orchestrates the creation of sites within the Cisco Catalyst Center, encompassing areas such as buildings and floors. It ensures necessary pre-checks are performed and allows for subsequent updates to these sites. Additionally, the module facilitates the deletion of specific sites using the site and parent names. A feature to delete all child sites by specifying only the parent site name is also available. + - The 'swim_workflow_manager' module handles the importation of SWIM images into the Cisco Catalyst Center, utilizing either a remote URL or a local image file path. It provides functionality for tagging and untagging SWIM images based on device family, role, and site. The module ensures the successful importation of images for distribution and activation on devices within the Cisco Catalyst Center. It also allows for the retrieval of a list of devices tied to a specific site, device family, and device role, facilitating various SWIM operations such as importing, tagging, distribution, and activation. + - The 'network_settings_workflow_manager' module manages global IP pool allocation, reserved sub pool assignment, and network function administration, including DHCP, Syslog, SNMP, NTP, Network AAA, client and endpoint AAA, and DNS servers, ensuring seamless operation at site and global levels in the Cisco Catalyst Center. + - The 'device_credential_workflow_manager' module oversees the management of global device credentials, including CLI, SNMPv2C read, SNMPv2C write, SNMPv3, HTTP(s) read, and HTTP(s) write. It facilitates the assignment of these credentials to specific sites, ensuring secure and efficient access to network devices across the infrastructure in the Cisco Catalyst Center. + - The 'inventory_workflow_manager' module is responsible for the actions that can be performed over devices which includes adding, deleting, resyncing, updating device details, device credentials, common info etc. for all types of devices - network device, compute device, meraki device, firepower management system device and third party devices. Exporting devices details and device credentials details into the CSV file, doing wired/wireless device provisioning, reboot AP devices, resyncing of device etc. Also we can update device just by giving the parameter that need to be changes on single or bulk devices and rest required parameters will be fetched from Cisco Catalyst Center and prepopulate it before triggering the update API. + - The 'pnp_workflow_manager' module helps in adding a device or adding devices in bulk to PnP database of the Cisco Catalyst Center. Post addition, device can be claimed to a site along with template provision and image upgrade. Along with that devices can be deleted from the PnP database. + - The 'discovery_workflow_manager' module streamlines the discovery of devices using various methods including single IP, IP range, multi-range, CDP, CIDR, and LLDP. It also offers the ability to clear out discoveries by deleting them from the discovery database, with an option to delete all discoveries simultaneously. + - The 'provision_workflow_manager' module provisions and re-provisions devices added in the inventory to site, by taking management IP address as input. It allows provisioning of both wired and wireless devices. It also allows un-provisioning of devices. + - The 'template_workflow_manager' module is responsible for overseeing templates, export projects/templates, and import projects/templates. It handles configuration templates by enabling the creation, updating, and deletion of templates and projects. Additionally, the module supports export functionality to retrieve project and template details from Cisco Catalyst Center, and Import functionality to create templates and projects within the Cisco Catalyst Center. diff --git a/galaxy.yml b/galaxy.yml index 7aeb8d456b..f5b2697877 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.10.4 +version: 6.11.0 readme: README.md authors: - Rafael Campos From 237c56f73c1739381d28b53c57b53755cf536282 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 20 Feb 2024 10:08:35 +0000 Subject: [PATCH 47/64] Adding fix for SWIM upgrade in PnP --- plugins/modules/pnp_intent.py | 43 +++++++++++++++++++++---- plugins/modules/pnp_workflow_manager.py | 43 +++++++++++++++++++++---- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 2592e5335f..3292437c79 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -447,6 +447,15 @@ def get_image_params(self, params): self.log("Image details are {0}".format(str(image_params)), "INFO") return image_params + def pnp_cred_failure(self, msg=None): + """ + Method for failing discovery if there is any discrepancy in the PnP credentials + passed by the user + """ + + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + def get_claim_params(self): """ Get the paramters needed for claiming the device to site. @@ -497,13 +506,31 @@ def get_claim_params(self): } if claim_params["type"] == "CatalystWLC": + if not (self.validated_config[0].get('static_ip')): + msg = "The static IP for claiming a wireless controller must be passed" + self.pnp_cred_failure(msg=msg) + if not (self.validated_config[0].get('subnet_mask')): + msg = "The subnet mask for claiming a wireless controller must be passed" + self.pnp_cred_failure(msg=msg) + if not (self.validated_config[0].get('gateway')): + msg = "The gateway IP for claiming a wireless controller must be of passed" + self.pnp_cred_failure(msg=msg) + if not (self.validated_config[0].get('ip_interface_name')): + msg = "The Interface Name for claiming a wireless controller must be passed" + self.pnp_cred_failure(msg=msg) + if not (self.validated_config[0].get('vlan_id')): + msg = "The Vlan Id for claiming a wireless controller must be passed" + self.pnp_cred_failure(msg=msg) claim_params["staticIP"] = self.validated_config[0]['static_ip'] claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] claim_params["gateway"] = self.validated_config[0]['gateway'] - claim_params["vlanId"] = str(self.validated_config[0]['vlan_id']) + claim_params["vlanId"] = str(self.validated_config[0].get('vlan_id')) claim_params["ipInterfaceName"] = self.validated_config[0]['ip_interface_name'] if claim_params["type"] == "AccessPoint": + if not (self.validated_config[0].get("rf_profile")): + msg = "The RF Profile for claiming an AP must be passed" + self.pnp_cred_failure(msg=msg) claim_params["rfProfile"] = self.validated_config[0]["rf_profile"] self.log("Paramters used for claiming are {0}".format(str(claim_params)), "INFO") @@ -633,17 +660,21 @@ def get_have(self): self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": - self.msg = "The site type must be specified as 'floor'\ - for claiming an AP" + self.msg = "The site type must be specified as 'floor' for claiming an AP" self.log(str(self.msg), "ERROR") self.status = "failed" return self + if len(image_list) == 0: + self.msg = "Either Image {0} is not present or is not tagged Golden"\ + " in Cisco Catalyst Center".format(self.validated_config[0].get("image_name")) + self.log(self.msg, "CRITICAL") + self.status = "failed" + return self + if len(image_list) == 1: if install_mode != "INSTALL": - self.msg = "Installation mode must be in \ - INSTALL mode to upgrade the image. Current mode is\ - {0}".format(install_mode) + self.msg = "Installation mode must be in INSTALL mode to upgrade the image. Current mode is {0}".format(install_mode) self.log(str(self.msg), "CRITICAL") self.status = "failed" return self diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 2081f2bb79..a8d9c18d20 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -447,6 +447,15 @@ def get_image_params(self, params): self.log("Image details are {0}".format(str(image_params)), "INFO") return image_params + def pnp_cred_failure(self, msg=None): + """ + Method for failing discovery if there is any discrepancy in the PnP credentials + passed by the user + """ + + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + def get_claim_params(self): """ Get the paramters needed for claiming the device to site. @@ -497,13 +506,31 @@ def get_claim_params(self): } if claim_params["type"] == "CatalystWLC": + if not (self.validated_config[0].get('static_ip')): + msg = "The static IP for claiming a wireless controller must be passed" + self.pnp_cred_failure(msg=msg) + if not (self.validated_config[0].get('subnet_mask')): + msg = "The subnet mask for claiming a wireless controller must be passed" + self.pnp_cred_failure(msg=msg) + if not (self.validated_config[0].get('gateway')): + msg = "The gateway IP for claiming a wireless controller must be of passed" + self.pnp_cred_failure(msg=msg) + if not (self.validated_config[0].get('ip_interface_name')): + msg = "The Interface Name for claiming a wireless controller must be passed" + self.pnp_cred_failure(msg=msg) + if not (self.validated_config[0].get('vlan_id')): + msg = "The Vlan Id for claiming a wireless controller must be passed" + self.pnp_cred_failure(msg=msg) claim_params["staticIP"] = self.validated_config[0]['static_ip'] claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] claim_params["gateway"] = self.validated_config[0]['gateway'] - claim_params["vlanId"] = str(self.validated_config[0]['vlan_id']) + claim_params["vlanId"] = str(self.validated_config[0].get('vlan_id')) claim_params["ipInterfaceName"] = self.validated_config[0]['ip_interface_name'] if claim_params["type"] == "AccessPoint": + if not (self.validated_config[0].get("rf_profile")): + msg = "The RF Profile for claiming an AP must be passed" + self.pnp_cred_failure(msg=msg) claim_params["rfProfile"] = self.validated_config[0]["rf_profile"] self.log("Paramters used for claiming are {0}".format(str(claim_params)), "INFO") @@ -633,17 +660,21 @@ def get_have(self): self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": - self.msg = "The site type must be specified as 'floor'\ - for claiming an AP" + self.msg = "The site type must be specified as 'floor' for claiming an AP" self.log(str(self.msg), "ERROR") self.status = "failed" return self + if len(image_list) == 0: + self.msg = "Either Image {0} is not present or is not tagged Golden"\ + " in Cisco Catalyst Center".format(self.validated_config[0].get("image_name")) + self.log(self.msg, "CRITICAL") + self.status = "failed" + return self + if len(image_list) == 1: if install_mode != "INSTALL": - self.msg = "Installation mode must be in \ - INSTALL mode to upgrade the image. Current mode is\ - {0}".format(install_mode) + self.msg = "Installation mode must be in INSTALL mode to upgrade the image. Current mode is {0}".format(install_mode) self.log(str(self.msg), "CRITICAL") self.status = "failed" return self From 34b442193987e0e5b067d80ccd8a3cad0b008a4b Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 20 Feb 2024 10:51:23 +0000 Subject: [PATCH 48/64] Adding fix for SWIM upgrade in PnP --- plugins/modules/pnp_intent.py | 25 +++++++++++++++---------- plugins/modules/pnp_workflow_manager.py | 25 +++++++++++++++---------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 3292437c79..dda994cc8e 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -507,19 +507,23 @@ def get_claim_params(self): if claim_params["type"] == "CatalystWLC": if not (self.validated_config[0].get('static_ip')): - msg = "The static IP for claiming a wireless controller must be passed" + msg = "A static IP address is required to claim a wireless controller. Please provide one." self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('subnet_mask')): - msg = "The subnet mask for claiming a wireless controller must be passed" + msg = "Please provide a subnet mask to claim a wireless controller. "\ + "This information is mandatory for the configuration." self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('gateway')): - msg = "The gateway IP for claiming a wireless controller must be of passed" + msg = "A gateway IP is required to claim a wireless controller. Please ensure to provide it." self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('ip_interface_name')): - msg = "The Interface Name for claiming a wireless controller must be passed" + msg = "Please provide the Interface Name to claim a wireless controller. This information is necessary"\ + " for making it a logical interface post claiming which can used to help manage the Wireless SSIDs "\ + "broadcasted by the access points, manage the controller, access point and user data, plus more" self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('vlan_id')): - msg = "The Vlan Id for claiming a wireless controller must be passed" + msg = "Please provide the Vlan ID to claim a wireless controller. This is a required field for the process"\ + " to create and set the specified port as trunk during PnP" self.pnp_cred_failure(msg=msg) claim_params["staticIP"] = self.validated_config[0]['static_ip'] claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] @@ -666,15 +670,16 @@ def get_have(self): return self if len(image_list) == 0: - self.msg = "Either Image {0} is not present or is not tagged Golden"\ - " in Cisco Catalyst Center".format(self.validated_config[0].get("image_name")) + self.msg = "The image '{0}' is either not present or not tagged as 'Golden' in the Cisco Catalyst Center."\ + " Please verify its existence and its tag status.".format(self.validated_config[0].get("image_name")) self.log(self.msg, "CRITICAL") self.status = "failed" return self if len(image_list) == 1: if install_mode != "INSTALL": - self.msg = "Installation mode must be in INSTALL mode to upgrade the image. Current mode is {0}".format(install_mode) + self.msg = "The system must be in INSTALL mode to upgrade the image. The current mode is '{0}'."\ + " Please switch to INSTALL mode to proceed".format(install_mode) self.log(str(self.msg), "CRITICAL") self.status = "failed" return self @@ -685,8 +690,8 @@ def get_have(self): template_name = self.want.get("template_name") if template_name: if not (template_list and isinstance(template_list, list)): - self.msg = "Either project not found \ - or it is Empty" + self.msg = "Either project not found" \ + "or it is Empty" self.log(self.msg, "CRITICAL") self.status = "failed" return self diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index a8d9c18d20..aa9974c371 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -507,19 +507,23 @@ def get_claim_params(self): if claim_params["type"] == "CatalystWLC": if not (self.validated_config[0].get('static_ip')): - msg = "The static IP for claiming a wireless controller must be passed" + msg = "A static IP address is required to claim a wireless controller. Please provide one." self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('subnet_mask')): - msg = "The subnet mask for claiming a wireless controller must be passed" + msg = "Please provide a subnet mask to claim a wireless controller. "\ + "This information is mandatory for the configuration." self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('gateway')): - msg = "The gateway IP for claiming a wireless controller must be of passed" + msg = "A gateway IP is required to claim a wireless controller. Please ensure to provide it." self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('ip_interface_name')): - msg = "The Interface Name for claiming a wireless controller must be passed" + msg = "Please provide the Interface Name to claim a wireless controller. This information is necessary"\ + " for making it a logical interface post claiming which can used to help manage the Wireless SSIDs "\ + "broadcasted by the access points, manage the controller, access point and user data, plus more" self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('vlan_id')): - msg = "The Vlan Id for claiming a wireless controller must be passed" + msg = "Please provide the Vlan ID to claim a wireless controller. This is a required field for the process"\ + " to create and set the specified port as trunk during PnP" self.pnp_cred_failure(msg=msg) claim_params["staticIP"] = self.validated_config[0]['static_ip'] claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] @@ -666,15 +670,16 @@ def get_have(self): return self if len(image_list) == 0: - self.msg = "Either Image {0} is not present or is not tagged Golden"\ - " in Cisco Catalyst Center".format(self.validated_config[0].get("image_name")) + self.msg = "The image '{0}' is either not present or not tagged as 'Golden' in the Cisco Catalyst Center."\ + " Please verify its existence and its tag status.".format(self.validated_config[0].get("image_name")) self.log(self.msg, "CRITICAL") self.status = "failed" return self if len(image_list) == 1: if install_mode != "INSTALL": - self.msg = "Installation mode must be in INSTALL mode to upgrade the image. Current mode is {0}".format(install_mode) + self.msg = "The system must be in INSTALL mode to upgrade the image. The current mode is '{0}'."\ + " Please switch to INSTALL mode to proceed".format(install_mode) self.log(str(self.msg), "CRITICAL") self.status = "failed" return self @@ -685,8 +690,8 @@ def get_have(self): template_name = self.want.get("template_name") if template_name: if not (template_list and isinstance(template_list, list)): - self.msg = "Either project not found \ - or it is Empty" + self.msg = "Either project not found" \ + "or it is Empty" self.log(self.msg, "CRITICAL") self.status = "failed" return self From 2cb4adcfb99e16f5ddf20553a7d9750aeda8436e Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 20 Feb 2024 11:23:01 +0000 Subject: [PATCH 49/64] Adding fix for SWIM upgrade in PnP --- plugins/modules/pnp_intent.py | 10 +++++----- plugins/modules/pnp_workflow_manager.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index dda994cc8e..0278ce1d92 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -519,11 +519,11 @@ def get_claim_params(self): if not (self.validated_config[0].get('ip_interface_name')): msg = "Please provide the Interface Name to claim a wireless controller. This information is necessary"\ " for making it a logical interface post claiming which can used to help manage the Wireless SSIDs "\ - "broadcasted by the access points, manage the controller, access point and user data, plus more" + "broadcasted by the access points, manage the controller, access point and user data, plus more." self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('vlan_id')): msg = "Please provide the Vlan ID to claim a wireless controller. This is a required field for the process"\ - " to create and set the specified port as trunk during PnP" + " to create and set the specified port as trunk during PnP." self.pnp_cred_failure(msg=msg) claim_params["staticIP"] = self.validated_config[0]['static_ip'] claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] @@ -679,7 +679,7 @@ def get_have(self): if len(image_list) == 1: if install_mode != "INSTALL": self.msg = "The system must be in INSTALL mode to upgrade the image. The current mode is '{0}'."\ - " Please switch to INSTALL mode to proceed".format(install_mode) + " Please switch to INSTALL mode to proceed.".format(install_mode) self.log(str(self.msg), "CRITICAL") self.status = "failed" return self @@ -691,7 +691,7 @@ def get_have(self): if template_name: if not (template_list and isinstance(template_list, list)): self.msg = "Either project not found" \ - "or it is Empty" + "or it is Empty". self.log(self.msg, "CRITICAL") self.status = "failed" return self @@ -707,7 +707,7 @@ def get_have(self): else: if not self.want.get('pnp_params')[0].get('deviceInfo'): - self.msg = "Either Site Name or Device details must be added" + self.msg = "Either Site Name or Device details must be added." self.log(self.msg, "ERROR") self.status = "failed" return self diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index aa9974c371..ab5beb4de0 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -519,11 +519,11 @@ def get_claim_params(self): if not (self.validated_config[0].get('ip_interface_name')): msg = "Please provide the Interface Name to claim a wireless controller. This information is necessary"\ " for making it a logical interface post claiming which can used to help manage the Wireless SSIDs "\ - "broadcasted by the access points, manage the controller, access point and user data, plus more" + "broadcasted by the access points, manage the controller, access point and user data, plus more." self.pnp_cred_failure(msg=msg) if not (self.validated_config[0].get('vlan_id')): msg = "Please provide the Vlan ID to claim a wireless controller. This is a required field for the process"\ - " to create and set the specified port as trunk during PnP" + " to create and set the specified port as trunk during PnP." self.pnp_cred_failure(msg=msg) claim_params["staticIP"] = self.validated_config[0]['static_ip'] claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] @@ -679,7 +679,7 @@ def get_have(self): if len(image_list) == 1: if install_mode != "INSTALL": self.msg = "The system must be in INSTALL mode to upgrade the image. The current mode is '{0}'."\ - " Please switch to INSTALL mode to proceed".format(install_mode) + " Please switch to INSTALL mode to proceed.".format(install_mode) self.log(str(self.msg), "CRITICAL") self.status = "failed" return self @@ -691,7 +691,7 @@ def get_have(self): if template_name: if not (template_list and isinstance(template_list, list)): self.msg = "Either project not found" \ - "or it is Empty" + "or it is Empty". self.log(self.msg, "CRITICAL") self.status = "failed" return self @@ -707,7 +707,7 @@ def get_have(self): else: if not self.want.get('pnp_params')[0].get('deviceInfo'): - self.msg = "Either Site Name or Device details must be added" + self.msg = "Either Site Name or Device details must be added." self.log(self.msg, "ERROR") self.status = "failed" return self From 41e369025c4b8559976ba556a4de1b6ff83ccebd Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 20 Feb 2024 11:31:59 +0000 Subject: [PATCH 50/64] Adding fix for SWIM upgrade in PnP --- plugins/modules/pnp_intent.py | 8 +++++--- plugins/modules/pnp_workflow_manager.py | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 0278ce1d92..91cfb871ef 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -664,7 +664,9 @@ def get_have(self): self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": - self.msg = "The site type must be specified as 'floor' for claiming an AP" + self.msg = "Please ensure that the site type is specified as 'floor' when claiming an AP."\ + "The site type is given as '{0}'. Please change the 'site_type' into 'floor' to"\ + "proceed.".format(self.get_site_type()) self.log(str(self.msg), "ERROR") self.status = "failed" return self @@ -690,8 +692,8 @@ def get_have(self): template_name = self.want.get("template_name") if template_name: if not (template_list and isinstance(template_list, list)): - self.msg = "Either project not found" \ - "or it is Empty". + self.msg = "Either project not found"\ + " or it is Empty". self.log(self.msg, "CRITICAL") self.status = "failed" return self diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index ab5beb4de0..21453eb7e4 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -664,7 +664,9 @@ def get_have(self): self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": - self.msg = "The site type must be specified as 'floor' for claiming an AP" + self.msg = "Please ensure that the site type is specified as 'floor' when claiming an AP."\ + "The site type is given as '{0}'. Please change the 'site_type' into 'floor' to"\ + "proceed.".format(self.get_site_type()) self.log(str(self.msg), "ERROR") self.status = "failed" return self @@ -690,8 +692,8 @@ def get_have(self): template_name = self.want.get("template_name") if template_name: if not (template_list and isinstance(template_list, list)): - self.msg = "Either project not found" \ - "or it is Empty". + self.msg = "Either project not found"\ + " or it is Empty". self.log(self.msg, "CRITICAL") self.status = "failed" return self From 1019df34346e1f851cfba400b2d468658c0bc06b Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 20 Feb 2024 11:33:52 +0000 Subject: [PATCH 51/64] Adding fix for SWIM upgrade in PnP --- plugins/modules/pnp_intent.py | 2 +- plugins/modules/pnp_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 91cfb871ef..4fcbb85bd6 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -665,7 +665,7 @@ def get_have(self): if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": self.msg = "Please ensure that the site type is specified as 'floor' when claiming an AP."\ - "The site type is given as '{0}'. Please change the 'site_type' into 'floor' to"\ + " The site type is given as '{0}'. Please change the 'site_type' into 'floor' to "\ "proceed.".format(self.get_site_type()) self.log(str(self.msg), "ERROR") self.status = "failed" diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 21453eb7e4..aa57d2c2ba 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -665,7 +665,7 @@ def get_have(self): if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": self.msg = "Please ensure that the site type is specified as 'floor' when claiming an AP."\ - "The site type is given as '{0}'. Please change the 'site_type' into 'floor' to"\ + " The site type is given as '{0}'. Please change the 'site_type' into 'floor' to "\ "proceed.".format(self.get_site_type()) self.log(str(self.msg), "ERROR") self.status = "failed" From 9516186aa8d699b3b956a0c732252a0fe791b1be Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 20 Feb 2024 11:36:25 +0000 Subject: [PATCH 52/64] Adding fix for SWIM upgrade in PnP --- plugins/modules/pnp_intent.py | 2 +- plugins/modules/pnp_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 4fcbb85bd6..1610ad35b4 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -693,7 +693,7 @@ def get_have(self): if template_name: if not (template_list and isinstance(template_list, list)): self.msg = "Either project not found"\ - " or it is Empty". + " or it is Empty." self.log(self.msg, "CRITICAL") self.status = "failed" return self diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index aa57d2c2ba..4165adc04f 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -693,7 +693,7 @@ def get_have(self): if template_name: if not (template_list and isinstance(template_list, list)): self.msg = "Either project not found"\ - " or it is Empty". + " or it is Empty." self.log(self.msg, "CRITICAL") self.status = "failed" return self From 59587f35279f8b77a84f710dbf7887d8f55a00b9 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 20 Feb 2024 11:40:01 +0000 Subject: [PATCH 53/64] Adding fix for SWIM upgrade in PnP --- plugins/modules/pnp_intent.py | 2 +- plugins/modules/pnp_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 1610ad35b4..36514657aa 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -664,7 +664,7 @@ def get_have(self): self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": - self.msg = "Please ensure that the site type is specified as 'floor' when claiming an AP."\ + self.msg = "Please ensure that the site type is specified as 'floor' when claiming an AP."\ " The site type is given as '{0}'. Please change the 'site_type' into 'floor' to "\ "proceed.".format(self.get_site_type()) self.log(str(self.msg), "ERROR") diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 4165adc04f..1d99876e1f 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -664,7 +664,7 @@ def get_have(self): self.log("Site Exists: {0}\nSite Name: {1}\nSite ID: {2}".format(site_exists, site_name, site_id), "INFO") if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": - self.msg = "Please ensure that the site type is specified as 'floor' when claiming an AP."\ + self.msg = "Please ensure that the site type is specified as 'floor' when claiming an AP."\ " The site type is given as '{0}'. Please change the 'site_type' into 'floor' to "\ "proceed.".format(self.get_site_type()) self.log(str(self.msg), "ERROR") From 2c67b0dc57c976d1e2a0aebe8037145451f7a261 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 20 Feb 2024 20:23:02 +0530 Subject: [PATCH 54/64] Add examples as playbook for each module SWIM, Site and Inventory, Fix the issue of updating snmp_protocol from ssh to telnet with netconf port, providing support to add multiple User Defined Field to single device. --- plugins/modules/inventory_intent.py | 464 ++++++++---------- plugins/modules/inventory_workflow_manager.py | 460 ++++++++--------- plugins/modules/site_intent.py | 54 +- plugins/modules/site_workflow_manager.py | 52 +- plugins/modules/swim_intent.py | 93 ++-- plugins/modules/swim_workflow_manager.py | 93 ++-- 6 files changed, 550 insertions(+), 666 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index cc9f186f8b..2677612699 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -255,7 +255,8 @@ type: str parameters: description: List of device parameters that needs to be exported to file. - type: str + type: list + elements: str managed_ap_locations: description: Location of the sites allocated for the APs type: list @@ -326,33 +327,30 @@ dnac_log: False state: merged config: - - cli_transport: string - compute_device: false - enable_password: string - extended_discovery_info: string - http_password: string - http_port: string - http_secure: false - http_username: string - ip_address: - - string - netconf_port: string - password: string - serial_number: string - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string - snmp_ro_community: string - snmp_rw_community: string + - cli_transport: ssh + compute_device: False + password: Test@123 + enable_password: Test@1234 + extended_discovery_info: test + http_username: "testuser" + http_password: "test" + http_port: "443" + http_secure: False + ip_address: ["1.1.1.1", "2.2.2.2"] + netconf_port: 830 + serial_number: FJC2327U0S2 + snmp_auth_passphrase: "Lablab@12" + snmp_auth_protocol: SHA + snmp_mode: AUTHPRIV + snmp_priv_passphrase: "Lablab@123" + snmp_priv_protocol: AES256 snmp_retry: 3 snmp_timeout: 5 - snmp_username: string - snmp_version: string - type: string + snmp_username: v3Public + snmp_version: v3 + type: NETWORK_DEVICE device_added: True - username: string + username: cisco - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device cisco.dnac.inventory_intent: @@ -367,21 +365,20 @@ dnac_log: False state: merged config: - - ip_address: - - string - http_username: string - http_password: string - http_port: string - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string + - ip_address: ["1.1.1.1", "2.2.2.2"] + http_username: "testuser" + http_password: "test" + http_port: "443" + snmp_auth_passphrase: "Lablab@12" + snmp_auth_protocol: SHA + snmp_mode: AUTHPRIV + snmp_priv_passphrase: "Lablab@123" + snmp_priv_protocol: AES256 snmp_retry: 3 snmp_timeout: 5 - snmp_username: string + snmp_username: v3Public compute_device: True - username: string + username: cisco device_added: True type: "COMPUTE_DEVICE" @@ -398,7 +395,7 @@ dnac_log: False state: merged config: - - http_password: string + - http_password: "test" device_added: True type: "MERAKI_DASHBOARD" @@ -415,11 +412,10 @@ dnac_log: False state: merged config: - - ip_address: - - string - http_username: string - http_password: string - http_port: string + - ip_address: ["1.1.1.1", "2.2.2.2"] + http_username: "testuser" + http_password: "test" + http_port: "443" device_added: True type: "FIREPOWER_MANAGEMENT_SYSTEM" @@ -436,16 +432,15 @@ dnac_log: False state: merged config: - - ip_address: - - string - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string + - ip_address: ["1.1.1.1", "2.2.2.2"] + snmp_auth_passphrase: "Lablab@12" + snmp_auth_protocol: SHA + snmp_mode: AUTHPRIV + snmp_priv_passphrase: "Lablab@123" + snmp_priv_protocol: AES256 snmp_retry: 3 snmp_timeout: 5 - snmp_username: string + snmp_username: v3Public device_added: True type: "THIRD_PARTY_DEVICE" @@ -462,33 +457,14 @@ dnac_log: False state: merged config: - - cli_transport: string - compute_device: false - password: string - enable_password: string - extended_discovery_info: string - http_password: string - http_port: string - http_secure: false - http_username: string - ip_address: - - string - netconf_port: string - serial_number: string - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string - snmp_username: string - snmp_version: string - type: string + - cli_transport: telnet + compute_device: False + password: newtest123 + enable_password: newtest1233 + ip_address: ["1.1.1.1", "2.2.2.2"] + type: NETWORK_DEVICE device_updated: True credential_update: True - update_mgmt_ipaddresslist: - - exist_mgmt_ipaddress: string - new_mgmt_ipaddress: string - username: string - name: Update new management IP address of device in inventory cisco.dnac.inventory_intent: @@ -504,12 +480,11 @@ state: merged config: - device_updated: True - ip_address: - - string + ip_address: ["1.1.1.1"] credential_update: True update_mgmt_ipaddresslist: - - exist_mgmt_ipaddress: string - new_mgmt_ipaddress: string + - exist_mgmt_ipaddress: "1.1.1.1" + new_mgmt_ipaddress: "12.12.12.12" - name: Associate Wired Devices to site and Provisioned it in Inventory cisco.dnac.inventory_intent: @@ -524,10 +499,9 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] provision_wired_device: - site_name: string + site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" - name: Associate Wireless Devices to site and Provisioned it in Inventory cisco.dnac.inventory_intent: @@ -542,19 +516,17 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] provision_wireless_device: - - site_name: string - managed_ap_locations: - - string + site_name: ["Global/USA/BGL_18/floor_pnp", "Global/USA/BGL_18/floor_test"] + managed_ap_locations: ["Global/USA/BGL_18/floor_pnp", "Global/USA/BGL_18/floor_test"] dynamic_interfaces: - - interface_ip_address: string - interface_netmask_in_cidr: int - interface_gateway: string - lag_or_port_number: int - vlan_id: int - interface_name: string + - interface_ip_address: 23.23.21.12 + interface_netmask_in_cidr: 24 + interface_gateway: "gateway" + lag_or_port_number: 12 + vlan_id: 99 + interface_name: "etherenet0/0" - name: Update Device Role with IP Address cisco.dnac.inventory_intent: @@ -569,12 +541,11 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] device_updated: True update_device_role: - role: string - role_source: string + role: ACCESS + role_source: AUTO - name: Update Interface details with IP Address cisco.dnac.inventory_intent: @@ -589,16 +560,15 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] device_updated: True update_interface_details: - description: str - admin_status: str - vlan_id: int - voice_vlan_id: int - deployment_mode: str - interface_name: str + description: "Testing for updating interface details" + admin_status: "UP" + vlan_id: 23 + voice_vlan_id: 45 + deployment_mode: "Deploy" + interface_name: GigabitEthernet1/0/11 - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_intent: @@ -613,12 +583,11 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] export_device_list: - password: str - operation_enum: str - parameters: str + password: "File_password" + operation_enum: 0 + parameters: ["componentName", "SerialNumber", "Last Sync Status"] - name: Create Global User Defined with IP Address cisco.dnac.inventory_intent: @@ -633,12 +602,14 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] add_user_defined_field: - name: string - description: string - value: string + - name: Test123 + description: "Added first udf for testing" + value: "value123" + - name: Test321 + description: "Added second udf for testing" + value: "value321" - name: Resync Device with IP Addresses cisco.dnac.inventory_intent: @@ -653,10 +624,9 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] device_resync: True - force_sync: false + force_sync: False - name: Reboot AP Devices with IP Addresses cisco.dnac.inventory_intent: @@ -671,8 +641,7 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] reboot_device: True - name: Delete Provision/Unprovision Devices by IP Address @@ -688,9 +657,8 @@ dnac_log_level: "{{dnac_log_level}}" state: deleted config: - - ip_address: - - string - clean_config: false + - ip_address: ["1.1.1.1", "2.2.2.2"] + clean_config: False - name: Delete Global User Defined Field with name cisco.dnac.inventory_intent: @@ -705,10 +673,10 @@ dnac_log: False state: deleted config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] add_user_defined_field: - name: string + - name: Test123 + - name: Test321 """ @@ -809,7 +777,7 @@ def validate_input(self): 'force_sync': {'type': 'bool'}, 'clean_config': {'type': 'bool'}, 'add_user_defined_field': { - 'type': 'dict', + 'type': 'list', 'name': {'type': 'str'}, 'description': {'type': 'str'}, 'value': {'type': 'str'}, @@ -825,7 +793,7 @@ def validate_input(self): 'type': 'dict', 'password': {'type': 'str'}, 'operation_enum': {'type': 'str'}, - 'parameters': {'type': 'str'}, + 'parameters': {'type': 'list', 'elements': 'str'}, }, 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, 'provision_wired_device': {'type': 'dict'}, @@ -968,11 +936,12 @@ def is_udf_exist(self, field_name): return False - def create_user_defined_field(self): + def create_user_defined_field(self, udf): """ Create a Global User Defined Field in Cisco Catalyst Center based on the provided configuration. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. + udf (dict): A dictionary having the payload for the creation of user defined field(UDF) in Cisco Catalyst Center. Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: @@ -980,15 +949,14 @@ def create_user_defined_field(self): sends the request to Cisco Catalyst Center to create the field, and logs the response. """ try: - payload = self.config[0].get('add_user_defined_field') response = self.dnac._exec( family="devices", function='create_user_defined_field', - params=payload, + params=udf, ) self.log("Received API response from 'create_user_defined_field': {0}".format(str(response)), "DEBUG") response = response.get("response") - field_name = self.config[0].get('add_user_defined_field').get('name') + field_name = udf.get('name') self.log("Global User Defined Field with name '{0}' created successfully".format(field_name), "INFO") self.status = "success" @@ -998,12 +966,13 @@ def create_user_defined_field(self): return self - def add_field_to_devices(self, device_ids): + def add_field_to_devices(self, device_ids, udf): """ Add a Global user-defined field with specified details to a list of devices in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ids (list): A list of device IDs to which the user-defined field will be added. + udf (dict): A dictionary having the user defined field details including name and value. Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: @@ -1011,9 +980,9 @@ def add_field_to_devices(self, device_ids): including the field name and default value then iterates over list of device IDs, creating a payload for each device and sending the request to Cisco Catalyst Center to add the user-defined field. """ - field_details = self.config[0].get('add_user_defined_field') - field_name = field_details.get('name') - field_value = field_details.get('value', '1') + # field_details = self.config[0].get('add_user_defined_field') + field_name = udf.get('name') + field_value = udf.get('value', '1') for device_id in device_ids: payload = {} payload['name'] = field_name @@ -1916,6 +1885,7 @@ def get_udf_id(self, field_name): """ try: + udf_id = None response = self.dnac._exec( family="devices", function='get_all_user_defined_fields', @@ -1923,7 +1893,8 @@ def get_udf_id(self, field_name): ) self.log("Received API response from 'get_all_user_defined_fields': {0}".format(str(response)), "DEBUG") udf = response.get("response") - udf_id = udf[0].get("id") + if udf: + udf_id = udf[0].get("id") except Exception as e: error_message = "Exception occurred while getting Global User Defined Fields(UDF) ID from Cisco Catalyst Center: {0}".format(str(e)) @@ -2668,40 +2639,6 @@ def get_diff_merged(self, config): device_reboot = self.config[0].get("reboot_device", False) credential_update = self.config[0].get("credential_update", False) - if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') - - if field_name is None: - self.status = "failed" - self.msg = "Error: The mandatory parameter 'name' for the User Defined Field is missing. Please provide the required information." - self.log(self.msg, "ERROR") - return self - - # Check if the Global User defined field exist if not then create it with given field name - udf_exist = self.is_udf_exist(field_name) - - if not udf_exist: - # Create the Global UDF - self.create_user_defined_field().check_return_status() - - # Get device Id based on config priority - device_ips = self.get_device_ips_from_config_priority() - device_ids = self.get_device_ids(device_ips) - - if len(device_ids) == 0: - self.status = "failed" - self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" - self.log(self.msg, "INFO") - self.result['changed'] = False - return self - - # Now add code for adding Global UDF to device with Id - self.add_field_to_devices(device_ids).check_return_status() - - self.result['changed'] = True - self.msg = "Global User Defined Field(UDF) named '{0}' has been successfully added to the device.".format(field_name) - self.log(self.msg, "INFO") - config['type'] = device_type if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": config['http_port'] = self.config[0].get("http_port", "443") @@ -2807,6 +2744,9 @@ def get_diff_merged(self, config): if playbook_params['netconfPort'] == " ": playbook_params['netconfPort'] = None + if playbook_params['netconfPort'] and playbook_params['cliTransport'] == "telnet": + playbook_params['netconfPort'] = None + try: if playbook_params['updateMgmtIPaddressList']: new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] @@ -2989,40 +2929,42 @@ def get_diff_merged(self, config): raise Exception(error_message) if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') + udf_field_list = self.config[0].get('add_user_defined_field') - if field_name is None: - self.status = "failed" - self.msg = "Mandatory paramter for User Define Field 'name' is missing" - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self + for udf in udf_field_list: + field_name = udf.get('name') - # Check if the Global User defined field exist if not then create it with given field name - udf_exist = self.is_udf_exist(field_name) + if field_name is None: + self.status = "failed" + self.msg = "Error: The mandatory parameter 'name' for the User Defined Field is missing. Please provide the required information." + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self - if not udf_exist: - # Create the Global UDF - self.create_user_defined_field().check_return_status() + # Check if the Global User defined field exist if not then create it with given field name + udf_exist = self.is_udf_exist(field_name) - # Get device Id based on config priority - device_ips = self.get_device_ips_from_config_priority() - device_ids = self.get_device_ids(device_ips) + if not udf_exist: + # Create the Global UDF + self.create_user_defined_field(udf).check_return_status() - if not device_ids: - self.status = "failed" - self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" - self.result['changed'] = False - self.result['response'] = self.msg - self.log(self.msg, "INFO") - return self + # Get device Id based on config priority + device_ips = self.get_device_ips_from_config_priority() + device_ids = self.get_device_ids(device_ips) + + if len(device_ids) == 0: + self.status = "failed" + self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" + self.log(self.msg, "INFO") + self.result['changed'] = False + return self - # Now add code for adding Global UDF to device with Id - self.add_field_to_devices(device_ids).check_return_status() + # Now add code for adding Global UDF to device with Id + self.add_field_to_devices(device_ids, udf).check_return_status() - self.result['changed'] = True - self.msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) - self.log(self.msg, "INFO") + self.result['changed'] = True + self.msg = "Global User Defined Field(UDF) named '{0}' has been successfully added to the device.".format(field_name) + self.log(self.msg, "INFO") # Once Wired device get added we will assign device to site and Provisioned it if self.config[0].get('provision_wired_device'): @@ -3062,51 +3004,54 @@ def get_diff_deleted(self, config): self.result['msg'] = [] if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') - udf_id = self.get_udf_id(field_name) + udf_field_list = self.config[0].get('add_user_defined_field') + for udf in udf_field_list: + field_name = udf.get('name') + udf_id = self.get_udf_id(field_name) - if udf_id is None: - self.status = "success" - self.msg = "Global UDF '{0}' is not present in Cisco Catalyst Center".format(field_name) - self.log(self.msg, "INFO") - self.result['changed'] = False - self.result['msg'] = self.msg - return self + if udf_id is None: + self.status = "success" + self.msg = "Global UDF '{0}' is not present in Cisco Catalyst Center".format(field_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['msg'] = self.msg + self.result['response'] = self.msg + return self - try: - response = self.dnac._exec( - family="devices", - function='delete_user_defined_field', - params={"id": udf_id}, - ) - if response and isinstance(response, dict): - self.log("Received API response from 'delete_user_defined_field': {0}".format(str(response)), "DEBUG") - task_id = response.get('response').get('taskId') + try: + response = self.dnac._exec( + family="devices", + function='delete_user_defined_field', + params={"id": udf_id}, + ) + if response and isinstance(response, dict): + self.log("Received API response from 'delete_user_defined_field': {0}".format(str(response)), "DEBUG") + task_id = response.get('response').get('taskId') - while True: - execution_details = self.get_task_details(task_id) + while True: + execution_details = self.get_task_details(task_id) - if 'success' in execution_details.get("progress"): - self.status = "success" - self.msg = "Global UDF '{0}' deleted successfully from Cisco Catalyst Center".format(field_name) - self.log(self.msg, "INFO") - self.result['changed'] = True - self.result['response'] = execution_details - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to delete Global User Defined Field(UDF) due to: {0}".format(failure_reason) - else: - self.msg = "Global UDF deletion get failed." - self.log(self.msg, "ERROR") - break + if 'success' in execution_details.get("progress"): + self.status = "success" + self.msg = "Global UDF '{0}' deleted successfully from Cisco Catalyst Center".format(field_name) + self.log(self.msg, "INFO") + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Failed to delete Global User Defined Field(UDF) due to: {0}".format(failure_reason) + else: + self.msg = "Global UDF deletion get failed." + self.log(self.msg, "ERROR") + break - except Exception as e: - error_message = "Error while deleting Global UDF from Cisco Catalyst Center: {0}".format(str(e)) - self.log(error_message, "ERROR") - raise Exception(error_message) + except Exception as e: + error_message = "Error while deleting Global UDF from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) return self @@ -3116,6 +3061,7 @@ def get_diff_deleted(self, config): self.result['changed'] = False self.msg = "Device '{0}' is not present in Cisco Catalyst Center so can't perform delete operation".format(device_ip) self.result['msg'] = self.msg + self.result['response'] = self.msg self.log(self.msg, "INFO") continue @@ -3261,16 +3207,18 @@ def verify_diff_merged(self, config): .format(device_type), "WARNING") if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') - udf_exist = self.is_udf_exist(field_name) + udf_field_list = self.config[0].get('add_user_defined_field') + for udf in udf_field_list: + field_name = udf.get('name') + udf_exist = self.is_udf_exist(field_name) - if udf_exist: - self.status = "success" - msg = "Global UDF {0} created and verified successfully".format(field_name) - self.log(msg, "INFO") - else: - self.log("""Mismatch between playbook parameter and Cisco Catalyst Center detected, indicating that - the task of creating Global UDF may not have executed successfully.""", "INFO") + if udf_exist: + self.status = "success" + msg = "Global UDF {0} created and verified successfully".format(field_name) + self.log(msg, "INFO") + else: + self.log("""Mismatch between playbook parameter and Cisco Catalyst Center detected, indicating that + the task of creating Global UDF may not have executed successfully.""", "INFO") if device_updated and self.config[0].get('update_device_role'): device_role_flag = True @@ -3326,14 +3274,18 @@ def verify_diff_deleted(self, config): device_in_dnac = self.device_exists_in_dnac() if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') - udf_id = self.get_udf_id(field_name) + udf_field_list = self.config[0].get('add_user_defined_field') + for udf in udf_field_list: + field_name = udf.get('name') + udf_id = self.get_udf_id(field_name) - if udf_id is None: - self.status = "success" - msg = "Global UDF named '{0}' has been successfully deleted from Cisco Catalyst Center and the deletion has been verified.".format(field_name) - self.log(msg, "INFO") - return self + if udf_id is None: + self.status = "success" + msg = """Global UDF named '{0}' has been successfully deleted from Cisco Catalyst Center and the deletion + has been verified.""".format(field_name) + self.log(msg, "INFO") + + return self device_delete_flag = True for device_ip in input_devices: @@ -3364,7 +3316,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + 'dnac_log_level': {'type': 'str', 'default': 'INFO'}, "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, "dnac_log_append": {"type": 'bool', "default": True}, 'dnac_log': {'type': 'bool', 'default': False}, diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index da9f933ed6..143af5bf9a 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -255,7 +255,8 @@ type: str parameters: description: List of device parameters that needs to be exported to file. - type: str + type: list + elements: str managed_ap_locations: description: Location of the sites allocated for the APs type: list @@ -326,33 +327,30 @@ dnac_log: False state: merged config: - - cli_transport: string - compute_device: false - enable_password: string - extended_discovery_info: string - http_password: string - http_port: string - http_secure: false - http_username: string - ip_address: - - string - netconf_port: string - password: string - serial_number: string - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string - snmp_ro_community: string - snmp_rw_community: string + - cli_transport: ssh + compute_device: False + password: Test@123 + enable_password: Test@1234 + extended_discovery_info: test + http_username: "testuser" + http_password: "test" + http_port: "443" + http_secure: False + ip_address: ["1.1.1.1", "2.2.2.2"] + netconf_port: 830 + serial_number: FJC2327U0S2 + snmp_auth_passphrase: "Lablab@12" + snmp_auth_protocol: SHA + snmp_mode: AUTHPRIV + snmp_priv_passphrase: "Lablab@123" + snmp_priv_protocol: AES256g snmp_retry: 3 snmp_timeout: 5 - snmp_username: string - snmp_version: string - type: string + snmp_username: v3Public + snmp_version: v3 + type: NETWORK_DEVICE device_added: True - username: string + username: cisco - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device cisco.dnac.inventory_workflow_manager: @@ -367,21 +365,20 @@ dnac_log: False state: merged config: - - ip_address: - - string - http_username: string - http_password: string - http_port: string - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string + - ip_address: ["1.1.1.1", "2.2.2.2"] + http_username: "testuser" + http_password: "test" + http_port: "443" + snmp_auth_passphrase: "Lablab@12" + snmp_auth_protocol: SHA + snmp_mode: AUTHPRIV + snmp_priv_passphrase: "Lablab@123" + snmp_priv_protocol: AES256 snmp_retry: 3 snmp_timeout: 5 - snmp_username: string + snmp_username: v3Public compute_device: True - username: string + username: cisco device_added: True type: "COMPUTE_DEVICE" @@ -398,7 +395,7 @@ dnac_log: False state: merged config: - - http_password: string + - http_password: "test" device_added: True type: "MERAKI_DASHBOARD" @@ -415,11 +412,10 @@ dnac_log: False state: merged config: - - ip_address: - - string - http_username: string - http_password: string - http_port: string + - ip_address: ["1.1.1.1", "2.2.2.2"] + http_username: "testuser" + http_password: "test" + http_port: "443" device_added: True type: "FIREPOWER_MANAGEMENT_SYSTEM" @@ -436,16 +432,15 @@ dnac_log: False state: merged config: - - ip_address: - - string - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string + - ip_address: ["1.1.1.1", "2.2.2.2"] + snmp_auth_passphrase: "Lablab@12" + snmp_auth_protocol: SHA + snmp_mode: AUTHPRIV + snmp_priv_passphrase: "Lablab@123" + snmp_priv_protocol: AES256 snmp_retry: 3 snmp_timeout: 5 - snmp_username: string + snmp_username: v3Public device_added: True type: "THIRD_PARTY_DEVICE" @@ -462,33 +457,14 @@ dnac_log: False state: merged config: - - cli_transport: string - compute_device: false - password: string - enable_password: string - extended_discovery_info: string - http_password: string - http_port: string - http_secure: false - http_username: string - ip_address: - - string - netconf_port: string - serial_number: string - snmp_auth_passphrase: string - snmp_auth_protocol: string - snmp_mode: string - snmp_priv_passphrase: string - snmp_priv_protocol: string - snmp_username: string - snmp_version: string - type: string + - cli_transport: telnet + compute_device: False + password: newtest123 + enable_password: newtest1233 + ip_address: ["1.1.1.1", "2.2.2.2"] + type: NETWORK_DEVICE device_updated: True credential_update: True - update_mgmt_ipaddresslist: - - exist_mgmt_ipaddress: string - new_mgmt_ipaddress: string - username: string - name: Update new management IP address of device in inventory cisco.dnac.inventory_workflow_manager: @@ -504,12 +480,11 @@ state: merged config: - device_updated: True - ip_address: - - string + ip_address: ["1.1.1.1"] credential_update: True update_mgmt_ipaddresslist: - - exist_mgmt_ipaddress: string - new_mgmt_ipaddress: string + - exist_mgmt_ipaddress: "1.1.1.1" + new_mgmt_ipaddress: "12.12.12.12" - name: Associate Wired Devices to site and Provisioned it in Inventory cisco.dnac.inventory_workflow_manager: @@ -524,10 +499,9 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] provision_wired_device: - site_name: string + site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" - name: Associate Wireless Devices to site and Provisioned it in Inventory cisco.dnac.inventory_workflow_manager: @@ -542,19 +516,17 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] provision_wireless_device: - - site_name: string - managed_ap_locations: - - string + site_name: ["Global/USA/BGL_18/floor_pnp", "Global/USA/BGL_18/floor_test"] + managed_ap_locations: ["Global/USA/BGL_18/floor_pnp", "Global/USA/BGL_18/floor_test"] dynamic_interfaces: - - interface_ip_address: string - interface_netmask_in_cidr: int - interface_gateway: string - lag_or_port_number: int - vlan_id: int - interface_name: string + - interface_ip_address: 23.23.21.12 + interface_netmask_in_cidr: 24 + interface_gateway: "gateway" + lag_or_port_number: 12 + vlan_id: 99 + interface_name: "etherenet0/0" - name: Update Device Role with IP Address cisco.dnac.inventory_workflow_manager: @@ -569,12 +541,11 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] device_updated: True update_device_role: - role: string - role_source: string + role: ACCESS + role_source: AUTO - name: Update Interface details with IP Address cisco.dnac.inventory_workflow_manager: @@ -589,16 +560,15 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] device_updated: True update_interface_details: - description: str - admin_status: str - vlan_id: int - voice_vlan_id: int - deployment_mode: str - interface_name: str + description: "Testing for updating interface details" + admin_status: "UP" + vlan_id: 23 + voice_vlan_id: 45 + deployment_mode: "Deploy" + interface_name: GigabitEthernet1/0/11 - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_workflow_manager: @@ -613,12 +583,11 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] export_device_list: - password: str - operation_enum: str - parameters: str + password: "File_password" + operation_enum: 0 + parameters: ["componentName", "SerialNumber", "Last Sync Status"] - name: Create Global User Defined with IP Address cisco.dnac.inventory_workflow_manager: @@ -633,12 +602,14 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] add_user_defined_field: - name: string - description: string - value: string + - name: Test123 + description: "Added first udf for testing" + value: "value123" + - name: Test321 + description: "Added second udf for testing" + value: "value321" - name: Resync Device with IP Addresses cisco.dnac.inventory_workflow_manager: @@ -653,10 +624,9 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] device_resync: True - force_sync: false + force_sync: False - name: Reboot AP Devices with IP Addresses cisco.dnac.inventory_workflow_manager: @@ -671,8 +641,7 @@ dnac_log: False state: merged config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] reboot_device: True - name: Delete Provision/Unprovision Devices by IP Address @@ -688,9 +657,8 @@ dnac_log_level: "{{dnac_log_level}}" state: deleted config: - - ip_address: - - string - clean_config: false + - ip_address: ["1.1.1.1", "2.2.2.2"] + clean_config: False - name: Delete Global User Defined Field with name cisco.dnac.inventory_workflow_manager: @@ -705,10 +673,9 @@ dnac_log: False state: deleted config: - - ip_address: - - string + - ip_address: ["1.1.1.1", "2.2.2.2"] add_user_defined_field: - name: string + name: "Test123" """ @@ -809,7 +776,7 @@ def validate_input(self): 'force_sync': {'type': 'bool'}, 'clean_config': {'type': 'bool'}, 'add_user_defined_field': { - 'type': 'dict', + 'type': 'list', 'name': {'type': 'str'}, 'description': {'type': 'str'}, 'value': {'type': 'str'}, @@ -825,7 +792,7 @@ def validate_input(self): 'type': 'dict', 'password': {'type': 'str'}, 'operation_enum': {'type': 'str'}, - 'parameters': {'type': 'str'}, + 'parameters': {'type': 'list', 'elements': 'str'}, }, 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, 'provision_wired_device': {'type': 'dict'}, @@ -968,11 +935,12 @@ def is_udf_exist(self, field_name): return False - def create_user_defined_field(self): + def create_user_defined_field(self, udf): """ Create a Global User Defined Field in Cisco Catalyst Center based on the provided configuration. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. + udf (dict): A dictionary having the payload for the creation of user defined field(UDF) in Cisco Catalyst Center. Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: @@ -980,15 +948,14 @@ def create_user_defined_field(self): sends the request to Cisco Catalyst Center to create the field, and logs the response. """ try: - payload = self.config[0].get('add_user_defined_field') response = self.dnac._exec( family="devices", function='create_user_defined_field', - params=payload, + params=udf, ) self.log("Received API response from 'create_user_defined_field': {0}".format(str(response)), "DEBUG") response = response.get("response") - field_name = self.config[0].get('add_user_defined_field').get('name') + field_name = udf.get('name') self.log("Global User Defined Field with name '{0}' created successfully".format(field_name), "INFO") self.status = "success" @@ -998,12 +965,13 @@ def create_user_defined_field(self): return self - def add_field_to_devices(self, device_ids): + def add_field_to_devices(self, device_ids, udf): """ Add a Global user-defined field with specified details to a list of devices in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ids (list): A list of device IDs to which the user-defined field will be added. + udf (dict): A dictionary having the user defined field details including name and value. Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: @@ -1011,9 +979,8 @@ def add_field_to_devices(self, device_ids): including the field name and default value then iterates over list of device IDs, creating a payload for each device and sending the request to Cisco Catalyst Center to add the user-defined field. """ - field_details = self.config[0].get('add_user_defined_field') - field_name = field_details.get('name') - field_value = field_details.get('value', '1') + field_name = udf.get('name') + field_value = udf.get('value', '1') for device_id in device_ids: payload = {} payload['name'] = field_name @@ -1917,6 +1884,7 @@ def get_udf_id(self, field_name): """ try: + udf_id = None response = self.dnac._exec( family="devices", function='get_all_user_defined_fields', @@ -1924,7 +1892,8 @@ def get_udf_id(self, field_name): ) self.log("Received API response from 'get_all_user_defined_fields': {0}".format(str(response)), "DEBUG") udf = response.get("response") - udf_id = udf[0].get("id") + if udf: + udf_id = udf[0].get("id") except Exception as e: error_message = "Exception occurred while getting Global User Defined Fields(UDF) ID from Cisco Catalyst Center: {0}".format(str(e)) @@ -2669,40 +2638,6 @@ def get_diff_merged(self, config): device_reboot = self.config[0].get("reboot_device", False) credential_update = self.config[0].get("credential_update", False) - if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') - - if field_name is None: - self.status = "failed" - self.msg = "Error: The mandatory parameter 'name' for the User Defined Field is missing. Please provide the required information." - self.log(self.msg, "ERROR") - return self - - # Check if the Global User defined field exist if not then create it with given field name - udf_exist = self.is_udf_exist(field_name) - - if not udf_exist: - # Create the Global UDF - self.create_user_defined_field().check_return_status() - - # Get device Id based on config priority - device_ips = self.get_device_ips_from_config_priority() - device_ids = self.get_device_ids(device_ips) - - if len(device_ids) == 0: - self.status = "failed" - self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" - self.log(self.msg, "INFO") - self.result['changed'] = False - return self - - # Now add code for adding Global UDF to device with Id - self.add_field_to_devices(device_ids).check_return_status() - - self.result['changed'] = True - self.msg = "Global User Defined Field(UDF) named '{0}' has been successfully added to the device.".format(field_name) - self.log(self.msg, "INFO") - config['type'] = device_type if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": config['http_port'] = self.config[0].get("http_port", "443") @@ -2810,6 +2745,9 @@ def get_diff_merged(self, config): if playbook_params['netconfPort'] == " ": playbook_params['netconfPort'] = None + if playbook_params['netconfPort'] and playbook_params['cliTransport'] == "telnet": + playbook_params['netconfPort'] = None + try: if playbook_params['updateMgmtIPaddressList']: new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] @@ -2992,40 +2930,43 @@ def get_diff_merged(self, config): raise Exception(error_message) if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') + udf_field_list = self.config[0].get('add_user_defined_field') - if field_name is None: - self.status = "failed" - self.msg = "Mandatory paramter for User Define Field 'name' is missing" - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self + for udf in udf_field_list: + field_name = udf.get('name') - # Check if the Global User defined field exist if not then create it with given field name - udf_exist = self.is_udf_exist(field_name) + if field_name is None: + self.status = "failed" + self.msg = "Error: The mandatory parameter 'name' for the User Defined Field is missing. Please provide the required information." + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self - if not udf_exist: - # Create the Global UDF - self.create_user_defined_field().check_return_status() + # Check if the Global User defined field exist if not then create it with given field name + udf_exist = self.is_udf_exist(field_name) - # Get device Id based on config priority - device_ips = self.get_device_ips_from_config_priority() - device_ids = self.get_device_ids(device_ips) + if not udf_exist: + # Create the Global UDF + self.create_user_defined_field(udf).check_return_status() - if not device_ids: - self.status = "failed" - self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" - self.result['changed'] = False - self.result['response'] = self.msg - self.log(self.msg, "INFO") - return self + # Get device Id based on config priority + device_ips = self.get_device_ips_from_config_priority() + device_ids = self.get_device_ids(device_ips) + + if not device_ids: + self.status = "failed" + self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" + self.result['changed'] = False + self.result['response'] = self.msg + self.log(self.msg, "INFO") + return self - # Now add code for adding Global UDF to device with Id - self.add_field_to_devices(device_ids).check_return_status() + # Now add code for adding Global UDF to device with Id + self.add_field_to_devices(device_ids, udf).check_return_status() - self.result['changed'] = True - self.msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) - self.log(self.msg, "INFO") + self.result['changed'] = True + self.msg = "Global User Defined Field(UDF) named '{0}' has been successfully added to the device.".format(field_name) + self.log(self.msg, "INFO") # Once Wired device get added we will assign device to site and Provisioned it if self.config[0].get('provision_wired_device'): @@ -3065,51 +3006,53 @@ def get_diff_deleted(self, config): self.result['msg'] = [] if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') - udf_id = self.get_udf_id(field_name) + udf_field_list = self.config[0].get('add_user_defined_field') + for udf in udf_field_list: + field_name = udf.get('name') + udf_id = self.get_udf_id(field_name) - if udf_id is None: - self.status = "success" - self.msg = "Global UDF '{0}' is not present in Cisco Catalyst Center".format(field_name) - self.log(self.msg, "INFO") - self.result['changed'] = False - self.result['msg'] = self.msg - return self + if udf_id is None: + self.status = "success" + self.msg = "Global UDF '{0}' is not present in Cisco Catalyst Center".format(field_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['msg'] = self.msg + return self - try: - response = self.dnac._exec( - family="devices", - function='delete_user_defined_field', - params={"id": udf_id}, - ) - if response and isinstance(response, dict): - self.log("Received API response from 'delete_user_defined_field': {0}".format(str(response)), "DEBUG") - task_id = response.get('response').get('taskId') + try: + response = self.dnac._exec( + family="devices", + function='delete_user_defined_field', + params={"id": udf_id}, + ) + if response and isinstance(response, dict): + self.log("Received API response from 'delete_user_defined_field': {0}".format(str(response)), "DEBUG") + task_id = response.get('response').get('taskId') - while True: - execution_details = self.get_task_details(task_id) + while True: + execution_details = self.get_task_details(task_id) - if 'success' in execution_details.get("progress"): - self.status = "success" - self.msg = "Global UDF '{0}' deleted successfully from Cisco Catalyst Center".format(field_name) - self.log(self.msg, "INFO") - self.result['changed'] = True - self.result['response'] = execution_details - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to delete Global User Defined Field(UDF) due to: {0}".format(failure_reason) - else: - self.msg = "Global UDF deletion get failed." - self.log(self.msg, "ERROR") - break + if 'success' in execution_details.get("progress"): + self.status = "success" + self.msg = "Global UDF '{0}' deleted successfully from Cisco Catalyst Center".format(field_name) + self.log(self.msg, "INFO") + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Failed to delete Global User Defined Field(UDF) due to: {0}".format(failure_reason) + else: + self.msg = "Global UDF deletion get failed." + self.log(self.msg, "ERROR") + break - except Exception as e: - error_message = "Error while deleting Global UDF from Cisco Catalyst Center: {0}".format(str(e)) - self.log(error_message, "ERROR") - raise Exception(error_message) + except Exception as e: + error_message = "Error while deleting Global UDF from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) return self @@ -3119,6 +3062,7 @@ def get_diff_deleted(self, config): self.result['changed'] = False self.msg = "Device '{0}' is not present in Cisco Catalyst Center so can't perform delete operation".format(device_ip) self.result['msg'] = self.msg + self.result['response'] = self.msg self.log(self.msg, "INFO") continue @@ -3264,16 +3208,18 @@ def verify_diff_merged(self, config): .format(device_type), "WARNING") if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') - udf_exist = self.is_udf_exist(field_name) + udf_field_list = self.config[0].get('add_user_defined_field') + for udf in udf_field_list: + field_name = udf.get('name') + udf_exist = self.is_udf_exist(field_name) - if udf_exist: - self.status = "success" - msg = "Global UDF {0} created and verified successfully".format(field_name) - self.log(msg, "INFO") - else: - self.log("""Mismatch between playbook parameter and Cisco Catalyst Center detected, indicating that - the task of creating Global UDF may not have executed successfully.""", "INFO") + if udf_exist: + self.status = "success" + msg = "Global UDF {0} created and verified successfully".format(field_name) + self.log(msg, "INFO") + else: + self.log("""Mismatch between playbook parameter and Cisco Catalyst Center detected, indicating that + the task of creating Global UDF may not have executed successfully.""", "INFO") if device_updated and self.config[0].get('update_device_role'): device_role_flag = True @@ -3329,14 +3275,18 @@ def verify_diff_deleted(self, config): device_in_ccc = self.device_exists_in_ccc() if self.config[0].get('add_user_defined_field'): - field_name = self.config[0].get('add_user_defined_field').get('name') - udf_id = self.get_udf_id(field_name) + udf_field_list = self.config[0].get('add_user_defined_field') + for udf in udf_field_list: + field_name = udf.get('name') + udf_id = self.get_udf_id(field_name) - if udf_id is None: - self.status = "success" - msg = "Global UDF named '{0}' has been successfully deleted from Cisco Catalyst Center and the deletion has been verified.".format(field_name) - self.log(msg, "INFO") - return self + if udf_id is None: + self.status = "success" + msg = """Global UDF named '{0}' has been successfully deleted from Cisco Catalyst Center and the deletion + has been verified.""".format(field_name) + self.log(msg, "INFO") + + return self device_delete_flag = True for device_ip in input_devices: diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 5fefe8589b..fab366f3c1 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -149,9 +149,9 @@ config: - site: area: - name: string - parent_name: string - site_type: string + name: Test + parent_name: Global/India + site_type: area - name: Create a new building site cisco.dnac.site_intent: @@ -168,12 +168,12 @@ config: - site: building: - address: string - latitude: float - longitude: float - name: string - parent_name: string - site_type: string + name: Building_1 + parent_name: Global/India + address: Bengaluru, Karnataka, India + latitude: 24.12 + longitude: 23.45 + site_type: building - name: Create a Floor site under the building cisco.dnac.site_intent: @@ -190,14 +190,14 @@ config: - site: floor: - name: string - parent_name: string - length: float - width: float - height: float - rf_model: string - floor_number: int - site_type: string + name: Floor_1 + parent_name: Global/India/Building_1 + length: 75.76 + width: 35.54 + height: 30.12 + rf_model: Cubes And Walled Offices + floor_number: 2 + site_type: floor - name: Updating the Floor details under the building cisco.dnac.site_intent: @@ -214,12 +214,12 @@ config: - site: floor: - name: string - parent_name: string - length: float - width: float - height: float - site_type: string + name: Floor_1 + parent_name: Global/India/Building_1 + length: 75.76 + width: 35.54 + height: 30.12 + site_type: floor - name: Deleting any site you need site name and parent name cisco.dnac.site_intent: @@ -236,9 +236,9 @@ config: - site: floor: - name: string - parent_name: string - site_type: string + name: Floor_1 + parent_name: Global/India/Building_1 + site_type: floor """ RETURN = r""" @@ -1040,7 +1040,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, + 'dnac_log_level': {'type': 'str', 'default': 'DEBUG'}, "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, "dnac_log_append": {"type": 'bool', "default": True}, 'dnac_log': {'type': 'bool', 'default': False}, diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index 737a2c5cc1..4e253a2ba5 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -149,9 +149,9 @@ config: - site: area: - name: string - parent_name: string - site_type: string + name: Test + parent_name: Global/India + site_type: area - name: Create a new building site cisco.dnac.site_workflow_manager: @@ -168,12 +168,12 @@ config: - site: building: - address: string - latitude: float - longitude: float - name: string - parent_name: string - site_type: string + name: Building_1 + parent_name: Global/India + address: Bengaluru, Karnataka, India + latitude: 24.12 + longitude: 23.45 + site_type: building - name: Create a Floor site under the building cisco.dnac.site_workflow_manager: @@ -190,14 +190,14 @@ config: - site: floor: - name: string - parent_name: string - length: float - width: float - height: float - rf_model: string - floor_number: int - site_type: string + name: Floor_1 + parent_name: Global/India/Building_1 + length: 75.76 + width: 35.54 + height: 30.12 + rf_model: Cubes And Walled Offices + floor_number: 2 + site_type: floor - name: Updating the Floor details under the building cisco.dnac.site_workflow_manager: @@ -214,12 +214,12 @@ config: - site: floor: - name: string - parent_name: string - length: float - width: float - height: float - site_type: string + name: Floor_1 + parent_name: Global/India/Building_1 + length: 75.76 + width: 35.54 + height: 30.12 + site_type: floor - name: Deleting any site you need site name and parent name cisco.dnac.site_workflow_manager: @@ -236,9 +236,9 @@ config: - site: floor: - name: string - parent_name: string - site_type: string + name: Floor_1 + parent_name: Global/India/Building_1 + site_type: floor """ RETURN = r""" diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 5fcc3c3c83..ea6c23136b 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -286,32 +286,26 @@ dnac_log: True config: - import_image_details: - type: string + type: remote url_details: payload: - - source_url: string - is_third_party: bool - image_family: string - vendor: string - application_type: string - schedule_at: string - schedule_desc: string - schedule_origin: string + - source_url: "http://10.10.10.10/stda/cat9k_iosxe.17.12.01.SPA.bin" + is_third_party: False tagging_details: - image_name: string - device_role: string - device_image_family_name: string - site_name: string - tagging: bool + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_role: ACCESS + device_image_family_name: Cisco Catalyst 9300 Switch + site_name: Global/USA/San Francisco/BGL_18 + tagging: True image_distribution_details: - image_name: string - device_serial_number: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_serial_number: FJC2327U0S2 image_activation_details: - schedule_validate: bool - activate_lower_image_version: bool - distribute_if_needed: bool - device_serial_number: string - image_name: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + schedule_validate: False + activate_lower_image_version: False + distribute_if_needed: True + device_serial_number: FJC2327U0S2 - name: Import an image from local, tag it as golden. cisco.dnac.swim_intent: @@ -326,19 +320,16 @@ dnac_log: True config: - import_image_details: - type: string + type: local local_image_details: - file_path: string - is_third_party: bool - third_party_vendor: string - third_party_image_family: string - third_party_application_type: string + file_path: /Users/Downloads/cat9k_iosxe.17.12.01.SPA.bin + is_third_party: False tagging_details: - image_name: string - device_role: string - device_image_family_name: string - site_name: string - tagging: bool + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_role: ACCESS + device_image_family_name: Cisco Catalyst 9300 Switch + site_name: Global/USA/San Francisco/BGL_18 + tagging: True - name: Tag the given image as golden and load it on device cisco.dnac.swim_intent: @@ -353,10 +344,10 @@ dnac_log: True config: - tagging_details: - image_name: string - device_role: string - device_image_family_name: string - site_name: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_role: ACCESS + device_image_family_name: Cisco Catalyst 9300 Switch + site_name: Global/USA/San Francisco/BGL_18 tagging: True - name: Un-tagged the given image as golden and load it on device @@ -372,10 +363,10 @@ dnac_log: True config: - tagging_details: - image_name: string - device_role: string - device_image_family_name: string - site_name: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_role: ACCESS + device_image_family_name: Cisco Catalyst 9300 Switch + site_name: Global/USA/San Francisco/BGL_18 tagging: False - name: Distribute the given image on devices associated to that site with specified role. @@ -391,10 +382,10 @@ dnac_log: True config: - image_distribution_details: - image_name: string - site_name: string - device_role: string - device_family_name: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + site_name: Global/USA/San Francisco/BGL_18 + device_role: ALL + device_family_name: Switches and Hubs - name: Activate the given image on devices associated to that site with specified role. cisco.dnac.swim_intent: @@ -409,13 +400,13 @@ dnac_log: True config: - image_activation_details: - image_name: string - site_name: string - device_role: string - device_family_name: string - scehdule_validate: bool - activate_lower_image_version: bool - distribute_if_needed: bool + image_name: cat9k_iosxe.17.12.01.SPA.bin + site_name: Global/USA/San Francisco/BGL_18 + device_role: ALL + device_family_name: Switches and Hubs + scehdule_validate: False + activate_lower_image_version: True + distribute_if_needed: True """ diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index ada17cfdf2..8374fb48a3 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -273,32 +273,26 @@ dnac_log: True config: - import_image_details: - type: string + type: remote url_details: payload: - - source_url: string - is_third_party: bool - image_family: string - vendor: string - application_type: string - schedule_at: string - schedule_desc: string - schedule_origin: string + - source_url: "http://10.10.10.10/stda/cat9k_iosxe.17.12.01.SPA.bin" + is_third_party: False tagging_details: - image_name: string - device_role: string - device_image_family_name: string - site_name: string - tagging: bool + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_role: ACCESS + device_image_family_name: Cisco Catalyst 9300 Switch + site_name: Global/USA/San Francisco/BGL_18 + tagging: True image_distribution_details: - image_name: string - device_serial_number: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_serial_number: FJC2327U0S2 image_activation_details: - schedule_validate: bool - activate_lower_image_version: bool - distribute_if_needed: bool - device_serial_number: string - image_name: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + schedule_validate: False + activate_lower_image_version: False + distribute_if_needed: True + device_serial_number: FJC2327U0S2 - name: Import an image from local, tag it as golden. cisco.dnac.swim_workflow_manager: @@ -313,19 +307,16 @@ dnac_log: True config: - import_image_details: - type: string + type: local local_image_details: - file_path: string - is_third_party: bool - third_party_vendor: string - third_party_image_family: string - third_party_application_type: string + file_path: /Users/Downloads/cat9k_iosxe.17.12.01.SPA.bin + is_third_party: False tagging_details: - image_name: string - device_role: string - device_image_family_name: string - site_name: string - tagging: bool + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_role: ACCESS + device_image_family_name: Cisco Catalyst 9300 Switch + site_name: Global/USA/San Francisco/BGL_18 + tagging: True - name: Tag the given image as golden and load it on device cisco.dnac.swim_workflow_manager: @@ -340,10 +331,10 @@ dnac_log: True config: - tagging_details: - image_name: string - device_role: string - device_image_family_name: string - site_name: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_role: ACCESS + device_image_family_name: Cisco Catalyst 9300 Switch + site_name: Global/USA/San Francisco/BGL_18 tagging: True - name: Un-tagged the given image as golden and load it on device @@ -359,10 +350,10 @@ dnac_log: True config: - tagging_details: - image_name: string - device_role: string - device_image_family_name: string - site_name: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + device_role: ACCESS + device_image_family_name: Cisco Catalyst 9300 Switch + site_name: Global/USA/San Francisco/BGL_18 tagging: False - name: Distribute the given image on devices associated to that site with specified role. @@ -378,10 +369,10 @@ dnac_log: True config: - image_distribution_details: - image_name: string - site_name: string - device_role: string - device_family_name: string + image_name: cat9k_iosxe.17.12.01.SPA.bin + site_name: Global/USA/San Francisco/BGL_18 + device_role: ALL + device_family_name: Switches and Hubs - name: Activate the given image on devices associated to that site with specified role. cisco.dnac.swim_workflow_manager: @@ -396,13 +387,13 @@ dnac_log: True config: - image_activation_details: - image_name: string - site_name: string - device_role: string - device_family_name: string - scehdule_validate: bool - activate_lower_image_version: bool - distribute_if_needed: bool + image_name: cat9k_iosxe.17.12.01.SPA.bin + site_name: Global/USA/San Francisco/BGL_18 + device_role: ALL + device_family_name: Switches and Hubs + scehdule_validate: False + activate_lower_image_version: True + distribute_if_needed: True """ From 1bf26480e63682e93aad8993bbbd579f72604e8e Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 20 Feb 2024 20:27:39 +0530 Subject: [PATCH 55/64] revert the dnac debug log level to WARNING --- plugins/modules/inventory_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 2677612699..b57895450a 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -3316,7 +3316,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'INFO'}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, "dnac_log_append": {"type": 'bool', "default": True}, 'dnac_log': {'type': 'bool', 'default': False}, From cbb1ea51f3a410864ced603a3e76683f41f70f20 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 20 Feb 2024 20:32:22 +0530 Subject: [PATCH 56/64] revert dnac debug level from DEBUG to WARNING in site intent module --- plugins/modules/site_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index fab366f3c1..2411b7b8df 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -1040,7 +1040,7 @@ def main(): 'dnac_verify': {'type': 'bool', 'default': 'True'}, 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, 'dnac_debug': {'type': 'bool', 'default': False}, - 'dnac_log_level': {'type': 'str', 'default': 'DEBUG'}, + 'dnac_log_level': {'type': 'str', 'default': 'WARNING'}, "dnac_log_file_path": {"type": 'str', "default": 'dnac.log'}, "dnac_log_append": {"type": 'bool', "default": True}, 'dnac_log': {'type': 'bool', 'default': False}, From c8a766d9a854cbd951e1f9c340f13c096302917b Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 20 Feb 2024 21:44:07 +0530 Subject: [PATCH 57/64] Add debug log messages --- plugins/modules/inventory_intent.py | 6 +++++- plugins/modules/inventory_workflow_manager.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index b57895450a..d8a848cd4b 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2745,6 +2745,8 @@ def get_diff_merged(self, config): playbook_params['netconfPort'] = None if playbook_params['netconfPort'] and playbook_params['cliTransport'] == "telnet": + self.log("""Updating the device cli transport from ssh to telnet with netconf port '{0}' so make + netconf port as None to perform the device update task""".format(playbook_params['netconfPort']), "DEBUG") playbook_params['netconfPort'] = None try: @@ -2946,6 +2948,7 @@ def get_diff_merged(self, config): if not udf_exist: # Create the Global UDF + self.log("Global User Defined Field '{0}' does not present in Cisco Catalyst Center, we need to create it".format(field_name), "DEBUG") self.create_user_defined_field(udf).check_return_status() # Get device Id based on config priority @@ -2954,7 +2957,8 @@ def get_diff_merged(self, config): if len(device_ids) == 0: self.status = "failed" - self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" + self.msg = """Unable to assign Global User Defined Field: No devices found in Cisco Catalyst Center. + Please add devices to proceed.""" self.log(self.msg, "INFO") self.result['changed'] = False return self diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 143af5bf9a..e20a78fe61 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -343,7 +343,7 @@ snmp_auth_protocol: SHA snmp_mode: AUTHPRIV snmp_priv_passphrase: "Lablab@123" - snmp_priv_protocol: AES256g + snmp_priv_protocol: AES256 snmp_retry: 3 snmp_timeout: 5 snmp_username: v3Public @@ -2746,6 +2746,8 @@ def get_diff_merged(self, config): playbook_params['netconfPort'] = None if playbook_params['netconfPort'] and playbook_params['cliTransport'] == "telnet": + self.log("""Updating the device cli transport from ssh to telnet with netconf port '{0}' so make + netconf port as None to perform the device update task""".format(playbook_params['netconfPort']), "DEBUG") playbook_params['netconfPort'] = None try: @@ -2947,6 +2949,7 @@ def get_diff_merged(self, config): if not udf_exist: # Create the Global UDF + self.log("Global User Defined Field '{0}' does not present in Cisco Catalyst Center, we need to create it".format(field_name), "DEBUG") self.create_user_defined_field(udf).check_return_status() # Get device Id based on config priority @@ -2955,7 +2958,8 @@ def get_diff_merged(self, config): if not device_ids: self.status = "failed" - self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco Catalyst Center" + self.msg = """Unable to assign Global User Defined Field: No devices found in Cisco Catalyst Center. + Please add devices to proceed.""" self.result['changed'] = False self.result['response'] = self.msg self.log(self.msg, "INFO") From 0e299a04d7e191eac45dd1f24b104e19a5f7ff8b Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 21 Feb 2024 12:51:42 +0530 Subject: [PATCH 58/64] Provide the support to update list of interfaces for a specific device and can extend it to list of devices as well --- plugins/modules/inventory_intent.py | 147 +++++++++--------- plugins/modules/inventory_workflow_manager.py | 147 +++++++++--------- 2 files changed, 152 insertions(+), 142 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index d8a848cd4b..5e43fdd432 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -75,20 +75,20 @@ hostname_list: description: "A list of hostnames representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses." - elements: str type: list + elements: str serial_number_list: description: A list of serial numbers representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses. - elements: str type: list + elements: str mac_address_list: description: "A list of MAC addresses representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses." - elements: str type: list + elements: str netconf_port: - description: Netconf port number. + description: Netconf port number(For example, 830). type: str username: description: Username for accessing the device. Required for Adding Network Device. @@ -233,14 +233,17 @@ admin_status: description: Status of Interface of a device, it can be (UP/DOWN). type: str + interface_name: + description: Specify the list of interface names to update the details of the device interface. + (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) + type: list + elements: str vlan_id: - description: Unique Id number assigned to a VLAN within a network. + description: Unique Id number assigned to a VLAN within a network used only while updating interface details. type: int voice_vlan_id: - description: Identifier used to distinguish a specific VLAN that is dedicated to voice traffic. + description: Identifier used to distinguish a specific VLAN that is dedicated to voice traffic used only while updating interface details. type: int - interface_name: - description: Specify the interface name to update the details of the device interface. (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) deployment_mode: description: Preview/Deploy [Preview means the configuration is not pushed to the device. Deploy makes the configuration pushed to the device] type: str @@ -568,7 +571,7 @@ vlan_id: 23 voice_vlan_id: 45 deployment_mode: "Deploy" - interface_name: GigabitEthernet1/0/11 + interface_name: ["GigabitEthernet1/0/11", FortyGigabitEthernet1/1/1] - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_intent: @@ -787,7 +790,7 @@ def validate_input(self): 'description': {'type': 'str'}, 'vlan_id': {'type': 'int'}, 'voice_vlan_id': {'type': 'int'}, - 'interface_name': {'type': 'str'}, + 'interface_name': {'type': 'list', 'elements': 'str'}, }, 'export_device_list': { 'type': 'dict', @@ -2446,68 +2449,69 @@ def update_interface_detail_of_device(self, device_to_update): # Call the Get interface details by device IP API and fetch the interface Id for device_ip in device_to_update: interface_params = self.config[0].get('update_interface_details') - interface_name = interface_params.get('interface_name') - device_id = self.get_device_ids([device_ip]) - interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) - self.check_return_status() + interface_names_list = interface_params.get('interface_name') + for interface_name in interface_names_list: + device_id = self.get_device_ids([device_ip]) + interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) + self.check_return_status() - # Now we call update interface details api with required parameter - try: - interface_params = self.config[0].get('update_interface_details') - temp_params = { - 'description': interface_params.get('description', ''), - 'adminStatus': interface_params.get('admin_status'), - 'voiceVlanId': interface_params.get('voice_vlan_id'), - 'vlanId': interface_params.get('vlan_id') - } - payload_params = {} - for key, value in temp_params.items(): - if value is not None: - payload_params[key] = value - - update_interface_params = { - 'payload': payload_params, - 'interface_uuid': interface_id, - 'deployment_mode': interface_params.get('deployment_mode', 'Deploy') - } - response = self.dnac._exec( - family="devices", - function='update_interface_details', - op_modifies=True, - params=update_interface_params, - ) - self.log("Received API response from 'update_interface_details': {0}".format(str(response)), "DEBUG") + # Now we call update interface details api with required parameter + try: + interface_params = self.config[0].get('update_interface_details') + temp_params = { + 'description': interface_params.get('description', ''), + 'adminStatus': interface_params.get('admin_status'), + 'voiceVlanId': interface_params.get('voice_vlan_id'), + 'vlanId': interface_params.get('vlan_id') + } + payload_params = {} + for key, value in temp_params.items(): + if value is not None: + payload_params[key] = value + + update_interface_params = { + 'payload': payload_params, + 'interface_uuid': interface_id, + 'deployment_mode': interface_params.get('deployment_mode', 'Deploy') + } + response = self.dnac._exec( + family="devices", + function='update_interface_details', + op_modifies=True, + params=update_interface_params, + ) + self.log("Received API response from 'update_interface_details': {0}".format(str(response)), "DEBUG") - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') - while True: - execution_details = self.get_task_details(task_id) + while True: + execution_details = self.get_task_details(task_id) - if 'SUCCESS' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Updated Interface Details for device '{0}' successfully".format(device_ip) - self.log(self.msg, "INFO") - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Interface Updation get failed because of {0}".format(failure_reason) - else: - self.msg = "Interface Updation get failed" - self.log(self.msg, "ERROR") - break + if 'SUCCESS' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Updated Interface Details for device '{0}' successfully".format(device_ip) + self.log(self.msg, "INFO") + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Interface Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Interface Updation get failed" + self.log(self.msg, "ERROR") + break - except Exception as e: - error_message = "Error while updating interface details in Cisco Catalyst Center: {0}".format(str(e)) - self.log(error_message, "INFO") - self.status = "success" - self.result['changed'] = False - self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" - self.log(self.msg, "INFO") + except Exception as e: + error_message = "Error while updating interface details in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "INFO") + self.status = "success" + self.result['changed'] = False + self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" + self.log(self.msg, "INFO") return self @@ -3182,12 +3186,13 @@ def verify_diff_merged(self, config): if device_updated and self.config[0].get('update_interface_details'): interface_update_flag = True - interface_name = self.config[0].get('update_interface_details').get('interface_name') + interface_names_list = self.config[0].get('update_interface_details').get('interface_name') for device_ip in device_ips: - if not self.check_interface_details(device_ip, interface_name): - interface_update_flag = False - break + for interface_name in interface_names_list: + if not self.check_interface_details(device_ip, interface_name): + interface_update_flag = False + break if interface_update_flag: self.status = "success" diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index e20a78fe61..ea13dd4108 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -75,20 +75,20 @@ hostname_list: description: "A list of hostnames representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses." - elements: str type: list + elements: str serial_number_list: description: A list of serial numbers representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses. - elements: str type: list + elements: str mac_address_list: description: "A list of MAC addresses representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses." - elements: str type: list + elements: str netconf_port: - description: Netconf port number. + description: Netconf port number(For example, 830). type: str username: description: Username for accessing the device. Required for Adding Network Device. @@ -233,14 +233,17 @@ admin_status: description: Status of Interface of a device, it can be (UP/DOWN). type: str + interface_name: + description: Specify the list of interface names to update the details of the device interface. + (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) + type: list + elements: str vlan_id: - description: Unique Id number assigned to a VLAN within a network. + description: Unique Id number assigned to a VLAN within a network used only while updating interface details. type: int voice_vlan_id: - description: Identifier used to distinguish a specific VLAN that is dedicated to voice traffic. + description: Identifier used to distinguish a specific VLAN that is dedicated to voice traffic used only while updating interface details. type: int - interface_name: - description: Specify the interface name to update the details of the device interface. (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) deployment_mode: description: Preview/Deploy [Preview means the configuration is not pushed to the device. Deploy makes the configuration pushed to the device] type: str @@ -568,7 +571,7 @@ vlan_id: 23 voice_vlan_id: 45 deployment_mode: "Deploy" - interface_name: GigabitEthernet1/0/11 + interface_name: ["GigabitEthernet1/0/11", FortyGigabitEthernet1/1/1] - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_workflow_manager: @@ -786,7 +789,7 @@ def validate_input(self): 'description': {'type': 'str'}, 'vlan_id': {'type': 'int'}, 'voice_vlan_id': {'type': 'int'}, - 'interface_name': {'type': 'str'}, + 'interface_name': {'type': 'list', 'elements': 'str'}, }, 'export_device_list': { 'type': 'dict', @@ -2445,68 +2448,69 @@ def update_interface_detail_of_device(self, device_to_update): # Call the Get interface details by device IP API and fetch the interface Id for device_ip in device_to_update: interface_params = self.config[0].get('update_interface_details') - interface_name = interface_params.get('interface_name') - device_id = self.get_device_ids([device_ip]) - interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) - self.check_return_status() + interface_names_list = interface_params.get('interface_name') + for interface_name in interface_names_list: + device_id = self.get_device_ids([device_ip]) + interface_id = self.get_interface_from_id_and_name(device_id[0], interface_name) + self.check_return_status() - # Now we call update interface details api with required parameter - try: - interface_params = self.config[0].get('update_interface_details') - temp_params = { - 'description': interface_params.get('description', ''), - 'adminStatus': interface_params.get('admin_status'), - 'voiceVlanId': interface_params.get('voice_vlan_id'), - 'vlanId': interface_params.get('vlan_id') - } - payload_params = {} - for key, value in temp_params.items(): - if value is not None: - payload_params[key] = value - - update_interface_params = { - 'payload': payload_params, - 'interface_uuid': interface_id, - 'deployment_mode': interface_params.get('deployment_mode', 'Deploy') - } - response = self.dnac._exec( - family="devices", - function='update_interface_details', - op_modifies=True, - params=update_interface_params, - ) - self.log("Received API response from 'update_interface_details': {0}".format(str(response)), "DEBUG") + # Now we call update interface details api with required parameter + try: + interface_params = self.config[0].get('update_interface_details') + temp_params = { + 'description': interface_params.get('description', ''), + 'adminStatus': interface_params.get('admin_status'), + 'voiceVlanId': interface_params.get('voice_vlan_id'), + 'vlanId': interface_params.get('vlan_id') + } + payload_params = {} + for key, value in temp_params.items(): + if value is not None: + payload_params[key] = value + + update_interface_params = { + 'payload': payload_params, + 'interface_uuid': interface_id, + 'deployment_mode': interface_params.get('deployment_mode', 'Deploy') + } + response = self.dnac._exec( + family="devices", + function='update_interface_details', + op_modifies=True, + params=update_interface_params, + ) + self.log("Received API response from 'update_interface_details': {0}".format(str(response)), "DEBUG") - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') - while True: - execution_details = self.get_task_details(task_id) + while True: + execution_details = self.get_task_details(task_id) - if 'SUCCESS' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Updated Interface Details for device '{0}' successfully".format(device_ip) - self.log(self.msg, "INFO") - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Interface Updation get failed because of {0}".format(failure_reason) - else: - self.msg = "Interface Updation get failed" - self.log(self.msg, "ERROR") - break + if 'SUCCESS' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Updated Interface Details for device '{0}' successfully".format(device_ip) + self.log(self.msg, "INFO") + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Interface Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Interface Updation get failed" + self.log(self.msg, "ERROR") + break - except Exception as e: - error_message = "Error while updating interface details in Cisco Catalyst Center: {0}".format(str(e)) - self.log(error_message, "INFO") - self.status = "success" - self.result['changed'] = False - self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" - self.log(self.msg, "INFO") + except Exception as e: + error_message = "Error while updating interface details in Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "INFO") + self.status = "success" + self.result['changed'] = False + self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" + self.log(self.msg, "INFO") return self @@ -3183,12 +3187,13 @@ def verify_diff_merged(self, config): if device_updated and self.config[0].get('update_interface_details'): interface_update_flag = True - interface_name = self.config[0].get('update_interface_details').get('interface_name') + interface_names_list = self.config[0].get('update_interface_details').get('interface_name') for device_ip in device_ips: - if not self.check_interface_details(device_ip, interface_name): - interface_update_flag = False - break + for interface_name in interface_names_list: + if not self.check_interface_details(device_ip, interface_name): + interface_update_flag = False + break if interface_update_flag: self.status = "success" From 1b887e01b40c86c7e045ea86c985e3078f5cdf57 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 21 Feb 2024 13:08:07 +0530 Subject: [PATCH 59/64] update the netconf port description in the documentation --- plugins/modules/inventory_intent.py | 4 +++- plugins/modules/inventory_workflow_manager.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 5e43fdd432..8c2f976a07 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -88,7 +88,9 @@ type: list elements: str netconf_port: - description: Netconf port number(For example, 830). + description: Specifies the port number for connecting to devices using the Netconf protocol. Netconf (Network Configuration Protocol) + is used for managing network devices. Ensure that the provided port number corresponds to the Netconf service port configured + on your network devices. type: str username: description: Username for accessing the device. Required for Adding Network Device. diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index ea13dd4108..e15dafff49 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -88,7 +88,9 @@ type: list elements: str netconf_port: - description: Netconf port number(For example, 830). + description: Specifies the port number for connecting to devices using the Netconf protocol. Netconf (Network Configuration Protocol) + is used for managing network devices. Ensure that the provided port number corresponds to the Netconf service port configured + on your network devices. type: str username: description: Username for accessing the device. Required for Adding Network Device. From 269d47fd531f41e5524cc3770ca79e907b9b5a0d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 21 Feb 2024 15:30:08 +0530 Subject: [PATCH 60/64] Update swim module documentation --- plugins/modules/swim_intent.py | 32 +++++++++++++++++++----- plugins/modules/swim_workflow_manager.py | 32 +++++++++++++++++++----- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index ea6c23136b..f864029ea1 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -83,9 +83,29 @@ application_type: description: An optional parameter that specifies the type of application. Allowed values include WLC, LINUX, FIREWALL, WINDOWS, LOADBALANCER, THIRDPARTY, etc. This is only applicable for third-party image types. + WLC (Wireless LAN Controller) - It's network device that manages and controls multiple wireless access points (APs) in a + centralized manner. + LINUX - It's an open source which provide complete operating system with a wide range of software packages and utilities. + FIREWALL - It's a network security device that monitors and controls incoming and outgoing network traffic based on + predetermined security rules.It acts as a barrier between a trusted internal network and untrusted external networks + (such as the internet), preventing unauthorized access. + WINDOWS - It's an OS which provides GUI support for various applications, and extensive compatibility with hardware + and software. + LOADBALANCER - It's a network device or software application that distributes incoming network traffic across multiple servers + or resources. + THIRDPARTY - It refers to third-party images or applications that are not part of the core system. + NAM (Network Access Manager) - It's a network management tool or software application that provides centralized control and + monitoring of network access policies, user authentication, and device compliance. + WAN Optimization - It refers to techniques and technologies used to improve the performance and efficiency of WANs. It includes + various optimization techniques such as data compression, caching, protocol optimization, and traffic prioritization to reduce + latency, increase throughput, and improve user experience over WAN connections. + Unknown - It refers to an unspecified or unrecognized application type. + Router - It's a network device that forwards data packets between computer networks. They are essential for connecting multiple + networks together and directing traffic between them. type: str image_family: - description: Represents the name of the image family and is applicable only when uploading third-party images (Optional). + description: Represents the name of the image family and is applicable only when uploading third-party images. Image Family name + like PALOALTO, RIVERBED, FORTINET, CHECKPOINT, SILVERPEAK etc. (optional). type: str source_url: description: A mandatory parameter for importing a SWIM image via a remote URL. This parameter is required when using a URL @@ -95,17 +115,17 @@ description: Flag indicates whether the image is uploaded from a third party (optional). type: bool vendor: - description: The name of the vendor, that applies only to third-party image types when importing via URL (Optional). + description: The name of the vendor, that applies only to third-party image types when importing via URL (optional). type: str schedule_at: description: ScheduleAt query parameter. Epoch Time (The number of milli-seconds since - January 1 1970 UTC) at which the distribution should be scheduled (Optional). + January 1 1970 UTC) at which the distribution should be scheduled (optional). type: str schedule_desc: - description: ScheduleDesc query parameter. Custom Description (Optional). + description: ScheduleDesc query parameter. Custom Description (optional). type: str schedule_origin: - description: ScheduleOrigin query parameter. Originator of this call (Optional). + description: ScheduleOrigin query parameter. Originator of this call (optional). type: str tagging_details: description: Details for tagging or untagging an image as golden @@ -252,7 +272,7 @@ type: str schedule_validate: description: ScheduleValidate query parameter. ScheduleValidate, validates data - before schedule (Optional). + before schedule (optional). type: bool requirements: - dnacentersdk == 2.4.5 diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 8374fb48a3..85d23dac7d 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -83,9 +83,29 @@ application_type: description: An optional parameter that specifies the type of application. Allowed values include WLC, LINUX, FIREWALL, WINDOWS, LOADBALANCER, THIRDPARTY, etc. This is only applicable for third-party image types. + WLC (Wireless LAN Controller) - It's network device that manages and controls multiple wireless access points (APs) in a + centralized manner. + LINUX - It's an open source which provide complete operating system with a wide range of software packages and utilities. + FIREWALL - It's a network security device that monitors and controls incoming and outgoing network traffic based on + predetermined security rules.It acts as a barrier between a trusted internal network and untrusted external networks + (such as the internet), preventing unauthorized access. + WINDOWS - It's an OS which provides GUI support for various applications, and extensive compatibility with hardware + and software. + LOADBALANCER - It's a network device or software application that distributes incoming network traffic across multiple servers + or resources. + THIRDPARTY - It refers to third-party images or applications that are not part of the core system. + NAM (Network Access Manager) - It's a network management tool or software application that provides centralized control and + monitoring of network access policies, user authentication, and device compliance. + WAN Optimization - It refers to techniques and technologies used to improve the performance and efficiency of WANs. It includes + various optimization techniques such as data compression, caching, protocol optimization, and traffic prioritization to reduce + latency, increase throughput, and improve user experience over WAN connections. + Unknown - It refers to an unspecified or unrecognized application type. + Router - It's a network device that forwards data packets between computer networks. They are essential for connecting multiple + networks together and directing traffic between them. type: str image_family: - description: Represents the name of the image family and is applicable only when uploading third-party images (Optional). + description: Represents the name of the image family and is applicable only when uploading third-party images. Image Family name + like PALOALTO, RIVERBED, FORTINET, CHECKPOINT, SILVERPEAK etc. (optional). type: str source_url: description: A mandatory parameter for importing a SWIM image via a remote URL. This parameter is required when using a URL @@ -95,17 +115,17 @@ description: Flag indicates whether the image is uploaded from a third party (optional). type: bool vendor: - description: The name of the vendor, that applies only to third-party image types when importing via URL (Optional). + description: The name of the vendor, that applies only to third-party image types when importing via URL (optional). type: str schedule_at: description: ScheduleAt query parameter. Epoch Time (The number of milli-seconds since - January 1 1970 UTC) at which the distribution should be scheduled (Optional). + January 1 1970 UTC) at which the distribution should be scheduled (optional). type: str schedule_desc: - description: ScheduleDesc query parameter. Custom Description (Optional). + description: ScheduleDesc query parameter. Custom Description (optional). type: str schedule_origin: - description: ScheduleOrigin query parameter. Originator of this call (Optional). + description: ScheduleOrigin query parameter. Originator of this call (optional). type: str tagging_details: description: Details for tagging or untagging an image as golden @@ -239,7 +259,7 @@ type: str schedule_validate: description: ScheduleValidate query parameter. ScheduleValidate, validates data - before schedule (Optional). + before schedule (optional). type: bool requirements: - dnacentersdk == 2.4.5 From 49dfe303656c40fea0164d6185e47043f39c77e7 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 21 Feb 2024 15:44:54 +0530 Subject: [PATCH 61/64] Add the documentation for third party application type and image family for local import as well --- plugins/modules/swim_intent.py | 27 +++++++++++++++++++++--- plugins/modules/swim_workflow_manager.py | 27 +++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index f864029ea1..ec58f5c2e0 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -63,10 +63,31 @@ description: Query parameter to determine if the image is from a third party (optional). type: bool third_party_application_type: - description: Specify the ThirdPartyApplicationType query parameter to indicate the type of third-party application.(optional) + description: Specify the ThirdPartyApplicationType query parameter to indicate the type of third-party application.Allowed + values include WLC, LINUX, FIREWALL, WINDOWS, LOADBALANCER, THIRDPARTY, etc.(optional). + WLC (Wireless LAN Controller) - It's network device that manages and controls multiple wireless access points (APs) in a + centralized manner. + LINUX - It's an open source which provide complete operating system with a wide range of software packages and utilities. + FIREWALL - It's a network security device that monitors and controls incoming and outgoing network traffic based on + predetermined security rules.It acts as a barrier between a trusted internal network and untrusted external networks + (such as the internet), preventing unauthorized access. + WINDOWS - It's an OS which provides GUI support for various applications, and extensive compatibility with hardware + and software. + LOADBALANCER - It's a network device or software application that distributes incoming network traffic across multiple servers + or resources. + THIRDPARTY - It refers to third-party images or applications that are not part of the core system. + NAM (Network Access Manager) - It's a network management tool or software application that provides centralized control and + monitoring of network access policies, user authentication, and device compliance. + WAN Optimization - It refers to techniques and technologies used to improve the performance and efficiency of WANs. It includes + various optimization techniques such as data compression, caching, protocol optimization, and traffic prioritization to reduce + latency, increase throughput, and improve user experience over WAN connections. + Unknown - It refers to an unspecified or unrecognized application type. + Router - It's a network device that forwards data packets between computer networks. They are essential for connecting multiple + networks together and directing traffic between them. type: str third_party_image_family: - description: Provide the ThirdPartyImageFamily query parameter to identify the family of the third-party image. (optional) + description: Provide the ThirdPartyImageFamily query parameter to identify the family of the third-party image. Image Family name + like PALOALTO, RIVERBED, FORTINET, CHECKPOINT, SILVERPEAK etc. (optional). type: str third_party_vendor: description: Include the ThirdPartyVendor query parameter to specify the vendor of the third party. @@ -82,7 +103,7 @@ suboptions: application_type: description: An optional parameter that specifies the type of application. Allowed values include WLC, LINUX, FIREWALL, WINDOWS, - LOADBALANCER, THIRDPARTY, etc. This is only applicable for third-party image types. + LOADBALANCER, THIRDPARTY, etc. This is only applicable for third-party image types(optional). WLC (Wireless LAN Controller) - It's network device that manages and controls multiple wireless access points (APs) in a centralized manner. LINUX - It's an open source which provide complete operating system with a wide range of software packages and utilities. diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 85d23dac7d..8a9c93bdcf 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -63,10 +63,31 @@ description: Query parameter to determine if the image is from a third party (optional). type: bool third_party_application_type: - description: Specify the ThirdPartyApplicationType query parameter to indicate the type of third-party application.(optional) + description: Specify the ThirdPartyApplicationType query parameter to indicate the type of third-party application.Allowed + values include WLC, LINUX, FIREWALL, WINDOWS, LOADBALANCER, THIRDPARTY, etc.(optional). + WLC (Wireless LAN Controller) - It's network device that manages and controls multiple wireless access points (APs) in a + centralized manner. + LINUX - It's an open source which provide complete operating system with a wide range of software packages and utilities. + FIREWALL - It's a network security device that monitors and controls incoming and outgoing network traffic based on + predetermined security rules.It acts as a barrier between a trusted internal network and untrusted external networks + (such as the internet), preventing unauthorized access. + WINDOWS - It's an OS which provides GUI support for various applications, and extensive compatibility with hardware + and software. + LOADBALANCER - It's a network device or software application that distributes incoming network traffic across multiple servers + or resources. + THIRDPARTY - It refers to third-party images or applications that are not part of the core system. + NAM (Network Access Manager) - It's a network management tool or software application that provides centralized control and + monitoring of network access policies, user authentication, and device compliance. + WAN Optimization - It refers to techniques and technologies used to improve the performance and efficiency of WANs. It includes + various optimization techniques such as data compression, caching, protocol optimization, and traffic prioritization to reduce + latency, increase throughput, and improve user experience over WAN connections. + Unknown - It refers to an unspecified or unrecognized application type. + Router - It's a network device that forwards data packets between computer networks. They are essential for connecting multiple + networks together and directing traffic between them. type: str third_party_image_family: - description: Provide the ThirdPartyImageFamily query parameter to identify the family of the third-party image. (optional) + description: Provide the ThirdPartyImageFamily query parameter to identify the family of the third-party image. Image Family name + like PALOALTO, RIVERBED, FORTINET, CHECKPOINT, SILVERPEAK etc. (optional). type: str third_party_vendor: description: Include the ThirdPartyVendor query parameter to specify the vendor of the third party. @@ -82,7 +103,7 @@ suboptions: application_type: description: An optional parameter that specifies the type of application. Allowed values include WLC, LINUX, FIREWALL, WINDOWS, - LOADBALANCER, THIRDPARTY, etc. This is only applicable for third-party image types. + LOADBALANCER, THIRDPARTY, etc. This is only applicable for third-party image types(optional). WLC (Wireless LAN Controller) - It's network device that manages and controls multiple wireless access points (APs) in a centralized manner. LINUX - It's an open source which provide complete operating system with a wide range of software packages and utilities. From 10681535807d713de3a19df833d79ffb18b62eaa Mon Sep 17 00:00:00 2001 From: Madhan Date: Wed, 21 Feb 2024 21:18:59 +0530 Subject: [PATCH 62/64] Added minor changes in changelog --- changelogs/changelog.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 422b6ed65b..9f2bdcfc31 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -796,7 +796,7 @@ releases: release_date: "2024-02-17" changes: release_summary: Adding new workflow manager modules in Cisco Catalyst Center - major_changes: + minor_changes: - The 'site_workflow_manager' module orchestrates the creation of sites within the Cisco Catalyst Center, encompassing areas such as buildings and floors. It ensures necessary pre-checks are performed and allows for subsequent updates to these sites. Additionally, the module facilitates the deletion of specific sites using the site and parent names. A feature to delete all child sites by specifying only the parent site name is also available. - The 'swim_workflow_manager' module handles the importation of SWIM images into the Cisco Catalyst Center, utilizing either a remote URL or a local image file path. It provides functionality for tagging and untagging SWIM images based on device family, role, and site. The module ensures the successful importation of images for distribution and activation on devices within the Cisco Catalyst Center. It also allows for the retrieval of a list of devices tied to a specific site, device family, and device role, facilitating various SWIM operations such as importing, tagging, distribution, and activation. - The 'network_settings_workflow_manager' module manages global IP pool allocation, reserved sub pool assignment, and network function administration, including DHCP, Syslog, SNMP, NTP, Network AAA, client and endpoint AAA, and DNS servers, ensuring seamless operation at site and global levels in the Cisco Catalyst Center. From 823a2ba3312d3c7cc69667c3488dc994095daa26 Mon Sep 17 00:00:00 2001 From: William Astorga Date: Wed, 21 Feb 2024 09:56:14 -0600 Subject: [PATCH 63/64] add smartquotes config to conf.py --- docs/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index e7b2f91071..8122fe4d69 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,6 +59,9 @@ '.md': 'markdown', } +# Avoid substitution of smartquotes +smartquotes = False + # The master toctree document. master_doc = 'index' From 72d11c394a6ffb0d7ed5af9b49e8bb5bced21f1c Mon Sep 17 00:00:00 2001 From: bvargasre Date: Wed, 21 Feb 2024 10:34:46 -0600 Subject: [PATCH 64/64] Update reame --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19e3f30621..f1ab07480c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The following table shows the supported versions. | 2.2.2.3 | 3.3.1 | 2.3.3 | | 2.2.3.3 | 6.4.0 | 2.4.11 | | 2.3.3.0 | 6.6.4 | 2.5.5 | -| 2.3.5.3 | 6.10.4 | 2.6.0 | +| 2.3.5.3 | 6.11.0 | 2.6.0 | If your Ansible collection is older please consider updating it first.