From 5f10867e3959fc6eb79a9390c8a5c42cd8cf3369 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 21 Feb 2024 19:47:21 +0530 Subject: [PATCH 01/44] Address PR review comments on swim module --- plugins/modules/swim_intent.py | 12 ++++++------ plugins/modules/swim_workflow_manager.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index ec58f5c2e0..308205ea5d 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -63,22 +63,22 @@ 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.Allowed + 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 + WLC (Wireless LAN Controller) - It's a 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. + LINUX - It's an open-source operating system that provides a complete set 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. + WINDOWS - It's an operating system known for its graphical user interface (GUI) support, extensive compatibility with hardware + and software, and widespread use across various applications. 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 + 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. diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 8a9c93bdcf..7148a514ac 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -63,22 +63,22 @@ 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.Allowed + 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 + WLC (Wireless LAN Controller) - It's a 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. + LINUX - It's an open-source operating system that provides a complete set 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. + WINDOWS - It's an operating system known for its graphical user interface (GUI) support, extensive compatibility with hardware + and software, and widespread use across various applications. 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 + 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. From 5c4d1d0ceb095355c914c37c786a21e26a968d15 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 22 Feb 2024 21:09:14 +0530 Subject: [PATCH 02/44] Handle the snmpPrivProtocol value as AES128, and change AES192/AES256 to CISCOAES192/CISCOAES256 respectively while adding/updating the device, Provide support to clear mac address table of specific interface, Remove serial_number, role - ALL, MD5 from the documentation, make role_source as MANUAL while updating device role. --- playbooks/inventory_workflow_manager.yml | 1 - plugins/modules/inventory_intent.py | 137 ++++++++++++++---- plugins/modules/inventory_workflow_manager.py | 135 +++++++++++++---- 3 files changed, 220 insertions(+), 53 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index 2ef5acbcfc..cf7998ed6f 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -43,7 +43,6 @@ 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}}" diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 8c2f976a07..6d27866cae 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -69,7 +69,7 @@ description: HTTP username required for adding compute and Firepower Management Devices. type: str ip_address: - description: IP address of the device. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. + description: List of IP address of the devices. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. elements: str type: list hostname_list: @@ -95,16 +95,12 @@ username: description: Username for accessing the device. Required for Adding Network Device. type: str - serial_number: - description: Serial number of the device. - type: str snmp_auth_passphrase: description: SNMP authentication passphrase required for adding network, compute, and third-party devices. type: str 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: @@ -177,34 +173,33 @@ 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 + default: False device_added: description: Make this as true needed for the addition of device in inventory. type: bool - default: false + 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 + default: False device_resync: description: Make this as true needed for the resyncing of device. type: bool - default: false + default: False reboot_device: description: Make this as true needed for the Rebooting of Access Points. type: bool - default: false + default: False credential_update: description: Make this as true needed for the updation of device credentials and other device details. type: bool - default: false + default: False clean_config: description: Required if need to delete the Provisioned device by clearing current configuration. type: bool - default: false + 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. @@ -219,10 +214,6 @@ providing interconnection between different network segments. 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 @@ -250,6 +241,10 @@ 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" + clear_mac_address_table: + description: Make this as true needed for clearing the mac address table of an interface of specific device. + type: bool + default: False site_name: description: Required for Provisioning of Wired and Wireless Devices. type: str @@ -343,7 +338,6 @@ 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 @@ -550,7 +544,6 @@ device_updated: True update_device_role: role: ACCESS - role_source: AUTO - name: Update Interface details with IP Address cisco.dnac.inventory_intent: @@ -574,6 +567,7 @@ voice_vlan_id: 45 deployment_mode: "Deploy" interface_name: ["GigabitEthernet1/0/11", FortyGigabitEthernet1/1/1] + clear_mac_address_table: True - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_intent: @@ -759,7 +753,6 @@ def validate_input(self): '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': {'type': 'str'}, @@ -793,6 +786,8 @@ def validate_input(self): 'vlan_id': {'type': 'int'}, 'voice_vlan_id': {'type': 'int'}, 'interface_name': {'type': 'list', 'elements': 'str'}, + 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, + 'clear_mac_address_table': {'default': False, 'type': 'bool'}, }, 'export_device_list': { 'type': 'dict', @@ -800,7 +795,6 @@ def validate_input(self): 'operation_enum': {'type': 'str'}, 'parameters': {'type': 'list', 'elements': 'str'}, }, - 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, 'provision_wired_device': {'type': 'dict'}, 'provision_wireless_device': { 'type': 'list', @@ -2018,7 +2012,6 @@ def get_device_params(self, params): "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"), @@ -2296,10 +2289,9 @@ def check_device_role(self, device_ip): 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 + return response.get('role') == role def check_interface_details(self, device_ip, interface_name): """ @@ -2433,6 +2425,71 @@ def get_provision_wired_device(self, device_ip): return True + def clear_mac_address(self, interface_id, deploy_mode, interface_name): + """ + Clear the MAC address table on a specific interface of a device. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + interface_id (str): The UUID of the interface where the MAC addresses will be cleared. + deploy_mode (str): The deployment mode of the device. + interface_name(str): The name of the interface for which the MAC addresses will be cleared. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function clears the MAC address table on a specific interface of a device. + The 'deploy_mode' parameter specifies the deployment mode of the device. + If the operation is successful, the function returns the response from the API call. + If an error occurs during the operation, the function logs the error details and updates the status accordingly. + """ + + try: + payload = { + "operation": "ClearMacAddress", + "payload": {} + } + clear_mac_address_payload = { + 'payload': payload, + 'interface_uuid': interface_id, + 'deployment_mode': deploy_mode + } + response = self.dnac._exec( + family="devices", + function='clear_mac_address_table', + op_modifies=True, + params=clear_mac_address_payload, + ) + self.log("Received API response from 'clear_mac_address_table': {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 execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Clearing the Mac address table for the interface '{0}' get failed because of {1}".format(interface_name, failure_reason) + else: + self.msg = "Clearing the Mac address table for the interface '{0}' get failed.".format(interface_name) + self.log(self.msg, "ERROR") + break + elif 'clear mac address-table' in execution_details.get("data"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Clearing Mac address table for interface '{0}' task executed successfully".format(interface_name) + self.log(self.msg, "INFO") + break + + except Exception as e: + error_msg = "Exception occured while clearing the mac address table of the interface {0} due to - {1}".format(interface_name, str(e)) + self.log(error_msg, "WARNING") + self.result['changed'] = False + + return self + def update_interface_detail_of_device(self, device_to_update): """ Update interface details for a device in Cisco Catalyst Center. @@ -2460,6 +2517,20 @@ def update_interface_detail_of_device(self, device_to_update): # Now we call update interface details api with required parameter try: interface_params = self.config[0].get('update_interface_details') + clear_mac_address_table = interface_params.get("clear_mac_address_table", False) + + if clear_mac_address_table: + response = self.get_device_response(device_ip) + + if response.get('role').upper() != "ACCESS": + self.msg = "Clearing MAC Address action is only supported on device with ACCESS role" + self.log(self.msg, "WARNING") + self.result['response'] = self.msg + else: + deploy_mode = interface_params.get('deployment_mode', 'Deploy') + self.clear_mac_address(interface_id, deploy_mode, interface_name) + self.check_return_status() + temp_params = { 'description': interface_params.get('description', ''), 'adminStatus': interface_params.get('admin_status'), @@ -2738,6 +2809,13 @@ def get_diff_merged(self, config): playbook_params['snmpAuthPassphrase'] = csv_data_dict['snmp_auth_passphrase'] playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] + if playbook_params['snmpPrivProtocol'] == "AES192": + playbook_params['snmpPrivProtocol'] = "CISCOAES192" + elif playbook_params['snmpPrivProtocol'] == "AES256": + playbook_params['snmpPrivProtocol'] = "CISCOAES256" + elif playbook_params['snmpPrivProtocol'] == "CISCOAES128": + playbook_params['snmpPrivProtocol'] = "AES128" + if playbook_params['snmpMode'] == "NOAUTHNOPRIV": playbook_params.pop('snmpAuthPassphrase', None) playbook_params.pop('snmpPrivPassphrase', None) @@ -2804,9 +2882,9 @@ def get_diff_merged(self, config): 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: + if 'role' not in device_role_args: self.status = "failed" - self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" + self.msg = "Mandatory paramter(role) to update Device Role is missing" self.log(self.msg, "WARNING") return self @@ -2827,7 +2905,7 @@ def get_diff_merged(self, config): device_role_params = { 'role': device_role_args.get('role'), - 'roleSource': device_role_args.get('role_source'), + 'roleSource': "MANUAL", 'id': device_id[0] } @@ -2881,6 +2959,11 @@ def get_diff_merged(self, config): if not device_params['snmpPrivProtocol']: device_params['snmpPrivProtocol'] = "AES128" + if device_params['snmpPrivProtocol'] == "AES192": + device_params['snmpPrivProtocol'] = "CISCOAES192" + elif device_params['snmpPrivProtocol'] == "AES256": + device_params['snmpPrivProtocol'] = "CISCOAES256" + if device_params['snmpMode'] == "NOAUTHNOPRIV": device_params.pop('snmpAuthPassphrase', None) device_params.pop('snmpPrivPassphrase', None) diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index e15dafff49..b9a20dcd5d 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -69,7 +69,7 @@ description: HTTP username required for adding compute and Firepower Management Devices. type: str ip_address: - description: IP address of the device. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. + description: List of IP address of the devices. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. elements: str type: list hostname_list: @@ -95,16 +95,12 @@ username: description: Username for accessing the device. Required for Adding Network Device. type: str - serial_number: - description: Serial number of the device. - type: str snmp_auth_passphrase: description: SNMP authentication passphrase required for adding network, compute, and third-party devices. type: str 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: @@ -177,31 +173,31 @@ 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 + default: False device_added: description: Make this as true needed for the addition of device in inventory. type: bool - default: false + 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 + default: False device_resync: description: Make this as true needed for the resyncing of device. type: bool - default: false + default: False reboot_device: description: Make this as true needed for the Rebooting of Access Points. type: bool - default: false + default: False credential_update: description: Make this as true needed for the updation of device credentials and other device details. type: bool - default: false + default: False clean_config: description: Required if need to delete the Provisioned device by clearing current configuration. type: bool - default: false + 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. @@ -219,10 +215,6 @@ providing interconnection between different network segments. 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 @@ -250,6 +242,10 @@ 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" + clear_mac_address_table: + description: Make this as true needed for clearing the mac address table of an interface of specific device. + type: bool + default: False site_name: description: Required for Provisioning of Wired and Wireless Devices. type: str @@ -343,7 +339,6 @@ 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 @@ -550,7 +545,6 @@ device_updated: True update_device_role: role: ACCESS - role_source: AUTO - name: Update Interface details with IP Address cisco.dnac.inventory_workflow_manager: @@ -574,6 +568,7 @@ voice_vlan_id: 45 deployment_mode: "Deploy" interface_name: ["GigabitEthernet1/0/11", FortyGigabitEthernet1/1/1] + clear_mac_address_table: True - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_workflow_manager: @@ -754,7 +749,6 @@ def validate_input(self): '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'}, @@ -792,6 +786,8 @@ def validate_input(self): 'vlan_id': {'type': 'int'}, 'voice_vlan_id': {'type': 'int'}, 'interface_name': {'type': 'list', 'elements': 'str'}, + 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, + 'clear_mac_address_table': {'default': False, 'type': 'bool'}, }, 'export_device_list': { 'type': 'dict', @@ -799,7 +795,6 @@ def validate_input(self): 'operation_enum': {'type': 'str'}, 'parameters': {'type': 'list', 'elements': 'str'}, }, - 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, 'provision_wired_device': {'type': 'dict'}, 'provision_wireless_device': { 'type': 'list', @@ -2295,10 +2290,9 @@ def check_device_role(self, device_ip): 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 + return response.get('role') == role def check_interface_details(self, device_ip, interface_name): """ @@ -2432,6 +2426,71 @@ def get_provision_wired_device(self, device_ip): return True + def clear_mac_address(self, interface_id, deploy_mode, interface_name): + """ + Clear the MAC address table on a specific interface of a device. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + interface_id (str): The UUID of the interface where the MAC addresses will be cleared. + deploy_mode (str): The deployment mode of the device. + interface_name(str): The name of the interface for which the MAC addresses will be cleared. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function clears the MAC address table on a specific interface of a device. + The 'deploy_mode' parameter specifies the deployment mode of the device. + If the operation is successful, the function returns the response from the API call. + If an error occurs during the operation, the function logs the error details and updates the status accordingly. + """ + + try: + payload = { + "operation": "ClearMacAddress", + "payload": {} + } + clear_mac_address_payload = { + 'payload': payload, + 'interface_uuid': interface_id, + 'deployment_mode': deploy_mode + } + response = self.dnac._exec( + family="devices", + function='clear_mac_address_table', + op_modifies=True, + params=clear_mac_address_payload, + ) + self.log("Received API response from 'clear_mac_address_table': {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 execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Clearing the Mac address table for the interface '{0}' get failed because of {1}".format(interface_name, failure_reason) + else: + self.msg = "Clearing the Mac address table for the interface '{0}' get failed.".format(interface_name) + self.log(self.msg, "ERROR") + break + elif 'clear mac address-table' in execution_details.get("data"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Clearing Mac address table for interface '{0}' task executed successfully".format(interface_name) + self.log(self.msg, "INFO") + break + + except Exception as e: + error_msg = "Exception occured while clearing the mac address table of the interface {0} due to - {1}".format(interface_name, str(e)) + self.log(error_msg, "WARNING") + self.result['changed'] = False + + return self + def update_interface_detail_of_device(self, device_to_update): """ Update interface details for a device in Cisco Catalyst Center. @@ -2459,6 +2518,20 @@ def update_interface_detail_of_device(self, device_to_update): # Now we call update interface details api with required parameter try: interface_params = self.config[0].get('update_interface_details') + clear_mac_address_table = interface_params.get("clear_mac_address_table", False) + + if clear_mac_address_table: + response = self.get_device_response(device_ip) + + if response.get('role').upper() != "ACCESS": + self.msg = "Clearing mac address action is only supported on device with ACCESS role" + self.log(self.msg, "WARNING") + self.result['response'] = self.msg + else: + deploy_mode = interface_params.get('deployment_mode', 'Deploy') + self.clear_mac_address(interface_id, deploy_mode, interface_name) + self.check_return_status() + temp_params = { 'description': interface_params.get('description', ''), 'adminStatus': interface_params.get('admin_status'), @@ -2739,6 +2812,13 @@ def get_diff_merged(self, config): if not playbook_params['snmpPrivPassphrase']: playbook_params['snmpPrivPassphrase'] = csv_data_dict['snmp_priv_passphrase'] + if playbook_params['snmpPrivProtocol'] == "AES192": + playbook_params['snmpPrivProtocol'] = "CISCOAES192" + elif playbook_params['snmpPrivProtocol'] == "AES256": + playbook_params['snmpPrivProtocol'] = "CISCOAES256" + elif playbook_params['snmpPrivProtocol'] == "CISCOAES128": + playbook_params['snmpPrivProtocol'] = "AES128" + if playbook_params['snmpMode'] == "NOAUTHNOPRIV": playbook_params.pop('snmpAuthPassphrase', None) playbook_params.pop('snmpPrivPassphrase', None) @@ -2805,9 +2885,9 @@ def get_diff_merged(self, config): 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: + if 'role' not in device_role_args: self.status = "failed" - self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" + self.msg = "Mandatory paramter(role) to update Device Role is missing" self.log(self.msg, "WARNING") return self @@ -2828,7 +2908,7 @@ def get_diff_merged(self, config): device_role_params = { 'role': device_role_args.get('role'), - 'roleSource': device_role_args.get('role_source'), + 'roleSource': "MANUAL", 'id': device_id[0] } @@ -2882,6 +2962,11 @@ def get_diff_merged(self, config): if not device_params['snmpPrivProtocol']: device_params['snmpPrivProtocol'] = "AES128" + if device_params['snmpPrivProtocol'] == "AES192": + device_params['snmpPrivProtocol'] = "CISCOAES192" + elif device_params['snmpPrivProtocol'] == "AES256": + device_params['snmpPrivProtocol'] = "CISCOAES256" + if device_params['snmpMode'] == "NOAUTHNOPRIV": device_params.pop('snmpAuthPassphrase', None) device_params.pop('snmpPrivPassphrase', None) From f0f5160ba3479ce3e91a60bff98c4c1267087fb0 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 23 Feb 2024 11:22:33 +0530 Subject: [PATCH 03/44] changes ip_address to ip_address list to avoid confusion and address review comments --- playbooks/inventory_workflow_manager.yml | 2 +- plugins/modules/inventory_intent.py | 75 ++++++++++--------- plugins/modules/inventory_workflow_manager.py | 75 ++++++++++--------- 3 files changed, 77 insertions(+), 75 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index cf7998ed6f..1646cfb408 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -24,7 +24,7 @@ - username: "{{item.username}}" password: "{{item.password}}" enable_password: "{{item.enable_password}}" - ip_address: "{{item.ip_address}}" + ip_address_list: "{{item.ip_address_list}}" cli_transport: "{{item.cli_transport}}" snmp_auth_passphrase: "{{item.snmp_auth_passphrase}}" snmp_auth_protocol: "{{item.snmp_auth_protocol}}" diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 6d27866cae..d4489fa48f 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -68,8 +68,9 @@ http_username: description: HTTP username required for adding compute and Firepower Management Devices. type: str - ip_address: - description: List of IP address of the devices. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. + ip_address_list: + description: A list of the IP addresses for the devices. It is required for tasks such as adding, updating, deleting, + or resyncing devices, with Meraki devices being the exception. elements: str type: list hostname_list: @@ -242,7 +243,8 @@ type: str default: "Deploy" clear_mac_address_table: - description: Make this as true needed for clearing the mac address table of an interface of specific device. + description: Set this to true if you need to clear the MAC address table for a specific device's interface. It's a boolean type, + with a default value of False. type: bool default: False site_name: @@ -336,7 +338,7 @@ http_password: "test" http_port: "443" http_secure: False - ip_address: ["1.1.1.1", "2.2.2.2"] + ip_address_list: ["1.1.1.1", "2.2.2.2"] netconf_port: 830 snmp_auth_passphrase: "Lablab@12" snmp_auth_protocol: SHA @@ -364,7 +366,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] http_username: "testuser" http_password: "test" http_port: "443" @@ -411,7 +413,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] http_username: "testuser" http_password: "test" http_port: "443" @@ -431,7 +433,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] snmp_auth_passphrase: "Lablab@12" snmp_auth_protocol: SHA snmp_mode: AUTHPRIV @@ -460,7 +462,7 @@ compute_device: False password: newtest123 enable_password: newtest1233 - ip_address: ["1.1.1.1", "2.2.2.2"] + ip_address_list: ["1.1.1.1", "2.2.2.2"] type: NETWORK_DEVICE device_updated: True credential_update: True @@ -479,7 +481,7 @@ state: merged config: - device_updated: True - ip_address: ["1.1.1.1"] + ip_address_list: ["1.1.1.1"] credential_update: True update_mgmt_ipaddresslist: - exist_mgmt_ipaddress: "1.1.1.1" @@ -498,7 +500,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] provision_wired_device: site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" @@ -515,7 +517,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] provision_wireless_device: 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"] @@ -540,7 +542,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] device_updated: True update_device_role: role: ACCESS @@ -558,7 +560,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] device_updated: True update_interface_details: description: "Testing for updating interface details" @@ -582,7 +584,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] export_device_list: password: "File_password" operation_enum: 0 @@ -601,7 +603,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] add_user_defined_field: - name: Test123 description: "Added first udf for testing" @@ -623,7 +625,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] device_resync: True force_sync: False @@ -640,7 +642,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] reboot_device: True - name: Delete Provision/Unprovision Devices by IP Address @@ -656,7 +658,7 @@ dnac_log_level: "{{dnac_log_level}}" state: deleted config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] clean_config: False - name: Delete Global User Defined Field with name @@ -672,7 +674,7 @@ dnac_log: False state: deleted config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] add_user_defined_field: - name: Test123 - name: Test321 @@ -747,7 +749,7 @@ def validate_input(self): 'http_port': {'type': 'str'}, 'http_secure': {'type': 'bool'}, 'http_username': {'type': 'str'}, - 'ip_address': {'type': 'list', 'elements': 'str'}, + 'ip_address_list': {'type': 'list', 'elements': 'str'}, 'hostname_list': {'type': 'list', 'elements': 'str'}, 'serial_number_list': {'type': 'list', 'elements': 'str'}, 'mac_address_list': {'type': 'list', 'elements': 'str'}, @@ -844,7 +846,7 @@ def get_device_ips_from_config_priority(self): 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") + device_ips = self.config[0].get("ip_address_list") if device_ips: return device_ips @@ -1256,7 +1258,7 @@ def resync_devices(self): 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. + - "ip_address_list": List of device IP addresses to be resynced. - "force_sync": (Optional) Whether to force sync the devices. Defaults to "False". """ @@ -1914,11 +1916,11 @@ def mandatory_parameter(self): 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"], + "NETWORK_DEVICE": ["enable_password", "ip_address_list", "password", "snmp_username", "username"], + "COMPUTE_DEVICE": ["ip_address_list", "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"] + "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address_list", "http_username", "http_password"], + "THIRD_PARTY_DEVICE": ["ip_address_list", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"] } params_list = params_dict.get(device_type, []) @@ -1993,7 +1995,7 @@ def get_device_params(self, params): "cliTransport": params.get("cli_transport"), "enablePassword": params.get("enable_password"), "password": params.get("password"), - "ipAddress": params.get("ip_address"), + "ipAddress": params.get("ip_address_list"), "snmpAuthPassphrase": params.get("snmp_auth_passphrase"), "snmpAuthProtocol": params.get("snmp_auth_protocol"), "snmpMode": params.get("snmp_mode"), @@ -2470,21 +2472,22 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Clearing the Mac address table for the interface '{0}' get failed because of {1}".format(interface_name, failure_reason) + self.msg = "Failed to clear the Mac address table for the interface '{0}' due to {1}".format(interface_name, failure_reason) else: - self.msg = "Clearing the Mac address table for the interface '{0}' get failed.".format(interface_name) + self.msg = "Failed to clear the Mac address table for the interface '{0}'".format(interface_name) self.log(self.msg, "ERROR") break elif 'clear mac address-table' in execution_details.get("data"): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.msg = "Clearing Mac address table for interface '{0}' task executed successfully".format(interface_name) + self.msg = "Successfully executed the task of clearing the Mac address table for interface '{0}'".format(interface_name) self.log(self.msg, "INFO") break except Exception as e: - error_msg = "Exception occured while clearing the mac address table of the interface {0} due to - {1}".format(interface_name, str(e)) + error_msg = """An exception occurred during the process of clearing the MAC address table for interface {0}, due to - + {1}""".format(interface_name, str(e)) self.log(error_msg, "WARNING") self.result['changed'] = False @@ -2523,7 +2526,7 @@ def update_interface_detail_of_device(self, device_to_update): response = self.get_device_response(device_ip) if response.get('role').upper() != "ACCESS": - self.msg = "Clearing MAC Address action is only supported on device with ACCESS role" + self.msg = "The action to clear the MAC Address table is only supported for devices with the ACCESS role." self.log(self.msg, "WARNING") self.result['response'] = self.msg else: @@ -2813,8 +2816,6 @@ def get_diff_merged(self, config): playbook_params['snmpPrivProtocol'] = "CISCOAES192" elif playbook_params['snmpPrivProtocol'] == "AES256": playbook_params['snmpPrivProtocol'] = "CISCOAES256" - elif playbook_params['snmpPrivProtocol'] == "CISCOAES128": - playbook_params['snmpPrivProtocol'] = "AES128" if playbook_params['snmpMode'] == "NOAUTHNOPRIV": playbook_params.pop('snmpAuthPassphrase', None) @@ -2884,7 +2885,7 @@ def get_diff_merged(self, config): if 'role' not in device_role_args: self.status = "failed" - self.msg = "Mandatory paramter(role) to update Device Role is missing" + self.msg = "Mandatory parameter (role) to update Device Role is missing" self.log(self.msg, "WARNING") return self @@ -2948,7 +2949,7 @@ def get_diff_merged(self, config): # If we want to add device in inventory if device_added: - config['ip_address'] = devices_to_add + config['ip_address_list'] = devices_to_add device_params = self.want.get("device_params") if not device_params['snmpMode']: device_params['snmpMode'] = "AUTHPRIV" @@ -2999,7 +3000,7 @@ def get_diff_merged(self, config): 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.msg = "Device(s) '{0}' already present in Cisco Catalyst Center".format(str(self.config[0].get("ip_address_list"))) self.log(self.msg, "INFO") self.result['msg'] = self.msg break diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index b9a20dcd5d..169ddf437c 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -68,8 +68,9 @@ http_username: description: HTTP username required for adding compute and Firepower Management Devices. type: str - ip_address: - description: List of IP address of the devices. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. + ip_address_list: + description: A list of the IP addresses for the devices. It is required for tasks such as adding, updating, deleting, + or resyncing devices, with Meraki devices being the exception. elements: str type: list hostname_list: @@ -243,7 +244,8 @@ type: str default: "Deploy" clear_mac_address_table: - description: Make this as true needed for clearing the mac address table of an interface of specific device. + description: Set this to true if you need to clear the MAC address table for a specific device's interface. It's a boolean type, + with a default value of False. type: bool default: False site_name: @@ -337,7 +339,7 @@ http_password: "test" http_port: "443" http_secure: False - ip_address: ["1.1.1.1", "2.2.2.2"] + ip_address_list: ["1.1.1.1", "2.2.2.2"] netconf_port: 830 snmp_auth_passphrase: "Lablab@12" snmp_auth_protocol: SHA @@ -365,7 +367,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] http_username: "testuser" http_password: "test" http_port: "443" @@ -412,7 +414,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] http_username: "testuser" http_password: "test" http_port: "443" @@ -432,7 +434,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] snmp_auth_passphrase: "Lablab@12" snmp_auth_protocol: SHA snmp_mode: AUTHPRIV @@ -461,7 +463,7 @@ compute_device: False password: newtest123 enable_password: newtest1233 - ip_address: ["1.1.1.1", "2.2.2.2"] + ip_address_list: ["1.1.1.1", "2.2.2.2"] type: NETWORK_DEVICE device_updated: True credential_update: True @@ -480,7 +482,7 @@ state: merged config: - device_updated: True - ip_address: ["1.1.1.1"] + ip_address_list: ["1.1.1.1"] credential_update: True update_mgmt_ipaddresslist: - exist_mgmt_ipaddress: "1.1.1.1" @@ -499,7 +501,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] provision_wired_device: site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" @@ -516,7 +518,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] provision_wireless_device: 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"] @@ -541,7 +543,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] device_updated: True update_device_role: role: ACCESS @@ -559,7 +561,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] device_updated: True update_interface_details: description: "Testing for updating interface details" @@ -583,7 +585,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] export_device_list: password: "File_password" operation_enum: 0 @@ -602,7 +604,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] add_user_defined_field: - name: Test123 description: "Added first udf for testing" @@ -624,7 +626,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] device_resync: True force_sync: False @@ -641,7 +643,7 @@ dnac_log: False state: merged config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] reboot_device: True - name: Delete Provision/Unprovision Devices by IP Address @@ -657,7 +659,7 @@ dnac_log_level: "{{dnac_log_level}}" state: deleted config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] clean_config: False - name: Delete Global User Defined Field with name @@ -673,7 +675,7 @@ dnac_log: False state: deleted config: - - ip_address: ["1.1.1.1", "2.2.2.2"] + - ip_address_list: ["1.1.1.1", "2.2.2.2"] add_user_defined_field: name: "Test123" @@ -747,7 +749,7 @@ def validate_input(self): 'http_port': {'type': 'str'}, 'http_secure': {'type': 'bool'}, 'http_username': {'type': 'str'}, - 'ip_address': {'type': 'list', 'elements': 'str'}, + 'ip_address_list': {'type': 'list', 'elements': 'str'}, 'hostname_list': {'type': 'list', 'elements': 'str'}, 'mac_address_list': {'type': 'list', 'elements': 'str'}, 'netconf_port': {'type': 'str'}, @@ -844,7 +846,7 @@ def get_device_ips_from_config_priority(self): 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") + device_ips = self.config[0].get("ip_address_list") if device_ips: return device_ips @@ -1255,7 +1257,7 @@ def resync_devices(self): 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. + - "ip_address_list": List of device IP addresses to be resynced. - "force_sync": (Optional) Whether to force sync the devices. Defaults to "False". """ @@ -1914,11 +1916,11 @@ def mandatory_parameter(self): 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"], + "NETWORK_DEVICE": ["enable_password", "ip_address_list", "password", "snmp_username", "username"], + "COMPUTE_DEVICE": ["ip_address_list", "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"] + "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address_list", "http_username", "http_password"], + "THIRD_PARTY_DEVICE": ["ip_address_list", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"] } params_list = params_dict.get(device_type, []) @@ -1993,7 +1995,7 @@ def get_device_params(self, params): "cliTransport": params.get("cli_transport"), "enablePassword": params.get("enable_password"), "password": params.get("password"), - "ipAddress": params.get("ip_address"), + "ipAddress": params.get("ip_address_list"), "snmpAuthPassphrase": params.get("snmp_auth_passphrase"), "snmpAuthProtocol": params.get("snmp_auth_protocol"), "snmpMode": params.get("snmp_mode"), @@ -2471,21 +2473,22 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): self.status = "failed" failure_reason = execution_details.get("failureReason") if failure_reason: - self.msg = "Clearing the Mac address table for the interface '{0}' get failed because of {1}".format(interface_name, failure_reason) + self.msg = "Failed to clear the Mac address table for the interface '{0}' due to {1}".format(interface_name, failure_reason) else: - self.msg = "Clearing the Mac address table for the interface '{0}' get failed.".format(interface_name) + self.msg = "Failed to clear the Mac address table for the interface '{0}'".format(interface_name) self.log(self.msg, "ERROR") break elif 'clear mac address-table' in execution_details.get("data"): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - self.msg = "Clearing Mac address table for interface '{0}' task executed successfully".format(interface_name) + self.msg = "Successfully executed the task of clearing the Mac address table for interface '{0}'".format(interface_name) self.log(self.msg, "INFO") break except Exception as e: - error_msg = "Exception occured while clearing the mac address table of the interface {0} due to - {1}".format(interface_name, str(e)) + error_msg = """An exception occurred during the process of clearing the MAC address table for interface {0}, due to - + {1}""".format(interface_name, str(e)) self.log(error_msg, "WARNING") self.result['changed'] = False @@ -2524,7 +2527,7 @@ def update_interface_detail_of_device(self, device_to_update): response = self.get_device_response(device_ip) if response.get('role').upper() != "ACCESS": - self.msg = "Clearing mac address action is only supported on device with ACCESS role" + self.msg = "The action to clear the MAC Address table is only supported for devices with the ACCESS role." self.log(self.msg, "WARNING") self.result['response'] = self.msg else: @@ -2816,8 +2819,6 @@ def get_diff_merged(self, config): playbook_params['snmpPrivProtocol'] = "CISCOAES192" elif playbook_params['snmpPrivProtocol'] == "AES256": playbook_params['snmpPrivProtocol'] = "CISCOAES256" - elif playbook_params['snmpPrivProtocol'] == "CISCOAES128": - playbook_params['snmpPrivProtocol'] = "AES128" if playbook_params['snmpMode'] == "NOAUTHNOPRIV": playbook_params.pop('snmpAuthPassphrase', None) @@ -2887,7 +2888,7 @@ def get_diff_merged(self, config): if 'role' not in device_role_args: self.status = "failed" - self.msg = "Mandatory paramter(role) to update Device Role is missing" + self.msg = "Mandatory parameter (role) to update Device Role is missing" self.log(self.msg, "WARNING") return self @@ -2951,7 +2952,7 @@ def get_diff_merged(self, config): # If we want to add device in inventory if device_added: - config['ip_address'] = devices_to_add + config['ip_address_list'] = devices_to_add device_params = self.want.get("device_params") if not device_params['snmpMode']: device_params['snmpMode'] = "AUTHPRIV" @@ -3002,7 +3003,7 @@ def get_diff_merged(self, config): 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.msg = "Device(s) '{0}' already present in Cisco Catalyst Center".format(str(self.config[0].get("ip_address_list"))) self.log(self.msg, "INFO") self.result['msg'] = self.msg break From b5db0ca0ade81443629500376f4470b9f2acd63d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 23 Feb 2024 11:43:41 +0530 Subject: [PATCH 04/44] optimise code by checking API response for clear_mac_address if empty then return from there itself --- plugins/modules/inventory_intent.py | 49 +++++++++++-------- plugins/modules/inventory_workflow_manager.py | 49 +++++++++++-------- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index d4489fa48f..b25ad3e2ef 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2462,34 +2462,43 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): ) self.log("Received API response from 'clear_mac_address_table': {0}".format(str(response)), "DEBUG") - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + if not response and isinstance(response, dict): + self.status = "failed" + self.msg = """Receive the empty response from 'clear_mac_address_table' API which indicates failed to clear + the Mac address table for the interface '{0}'""".format(interface_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self - while True: - execution_details = self.get_task_details(task_id) + task_id = response.get('response').get('taskId') - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to clear the Mac address table for the interface '{0}' due to {1}".format(interface_name, failure_reason) - else: - self.msg = "Failed to clear the Mac address table for the interface '{0}'".format(interface_name) - self.log(self.msg, "ERROR") - break - elif 'clear mac address-table' in execution_details.get("data"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Successfully executed the task of clearing the Mac address table for interface '{0}'".format(interface_name) - self.log(self.msg, "INFO") - break + 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 = "Failed to clear the Mac address table for the interface '{0}' due to {1}".format(interface_name, failure_reason) + else: + self.msg = "Failed to clear the Mac address table for the interface '{0}'".format(interface_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + break + elif 'clear mac address-table' in execution_details.get("data"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Successfully executed the task of clearing the Mac address table for interface '{0}'".format(interface_name) + self.log(self.msg, "INFO") + break except Exception as e: error_msg = """An exception occurred during the process of clearing the MAC address table for interface {0}, due to - {1}""".format(interface_name, str(e)) self.log(error_msg, "WARNING") self.result['changed'] = False + self.result['response'] = error_msg return self diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 169ddf437c..55ea02122d 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -2463,34 +2463,43 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): ) self.log("Received API response from 'clear_mac_address_table': {0}".format(str(response)), "DEBUG") - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + if not response and isinstance(response, dict): + self.status = "failed" + self.msg = """Receive the empty response from 'clear_mac_address_table' API which indicates failed to clear + the Mac address table for the interface '{0}'""".format(interface_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self - while True: - execution_details = self.get_task_details(task_id) + task_id = response.get('response').get('taskId') - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to clear the Mac address table for the interface '{0}' due to {1}".format(interface_name, failure_reason) - else: - self.msg = "Failed to clear the Mac address table for the interface '{0}'".format(interface_name) - self.log(self.msg, "ERROR") - break - elif 'clear mac address-table' in execution_details.get("data"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Successfully executed the task of clearing the Mac address table for interface '{0}'".format(interface_name) - self.log(self.msg, "INFO") - break + 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 = "Failed to clear the Mac address table for the interface '{0}' due to {1}".format(interface_name, failure_reason) + else: + self.msg = "Failed to clear the Mac address table for the interface '{0}'".format(interface_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + break + elif 'clear mac address-table' in execution_details.get("data"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Successfully executed the task of clearing the Mac address table for interface '{0}'".format(interface_name) + self.log(self.msg, "INFO") + break except Exception as e: error_msg = """An exception occurred during the process of clearing the MAC address table for interface {0}, due to - {1}""".format(interface_name, str(e)) self.log(error_msg, "WARNING") self.result['changed'] = False + self.result['response'] = error_msg return self From bef66526eb75587f1bf8c0f7a423a1b64bbe7d0a Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 23 Feb 2024 12:17:39 +0530 Subject: [PATCH 05/44] put the reponse check in bracket --- plugins/modules/inventory_intent.py | 4 ++-- plugins/modules/inventory_workflow_manager.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index b25ad3e2ef..77baa02722 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2462,9 +2462,9 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): ) self.log("Received API response from 'clear_mac_address_table': {0}".format(str(response)), "DEBUG") - if not response and isinstance(response, dict): + if not (response and isinstance(response, dict)): self.status = "failed" - self.msg = """Receive the empty response from 'clear_mac_address_table' API which indicates failed to clear + self.msg = """Received an empty response from the API 'clear_mac_address_table'. This indicates a failure to clear the Mac address table for the interface '{0}'""".format(interface_name) self.log(self.msg, "ERROR") self.result['response'] = self.msg diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 55ea02122d..b354fe875e 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -2463,9 +2463,9 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): ) self.log("Received API response from 'clear_mac_address_table': {0}".format(str(response)), "DEBUG") - if not response and isinstance(response, dict): + if not (response and isinstance(response, dict)): self.status = "failed" - self.msg = """Receive the empty response from 'clear_mac_address_table' API which indicates failed to clear + self.msg = """Received an empty response from the API 'clear_mac_address_table'. This indicates a failure to clear the Mac address table for the interface '{0}'""".format(interface_name) self.log(self.msg, "ERROR") self.result['response'] = self.msg From e6c64470dacf60e1a151089206d0063b98dfca2d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 23 Feb 2024 20:07:39 +0530 Subject: [PATCH 06/44] Write the API for Wired Device Provisioning with new format having resync_retry_count and resync_interval_count, Remove device_added field from the intent, workflow manager module and make it idempotent operation. --- playbooks/inventory_workflow_manager.yml | 18 +- plugins/modules/inventory_intent.py | 211 ++++++++++------- plugins/modules/inventory_workflow_manager.py | 212 +++++++++++------- 3 files changed, 268 insertions(+), 173 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index 1646cfb408..0c284b83c7 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -38,16 +38,26 @@ 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}}" add_user_defined_field: - name: "{{item.name}}" - description: "{{item.description}}" - value: "{{item.value}}" + - name: Test123 + description: "Added first udf for testing" + value: "value123" + - name: Test321 + description: "Added second udf for testing" + value: "value321" provision_wired_device: + - device_ip: "1.1.1.1" + site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" + resync_retry_count: 200 + resync_interval: 2 + - device_ip: "2.2.2.2" + site_name: "Global/USA/San Francisco/BGL_18/floor_test" + resync_retry_count: 200 + resync_retry_interval: 2 site_name: "{{item.site_name}}" update_interface_details: description: "{{item.update_interface_details.description}}" diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 77baa02722..2a02fa1e22 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -149,13 +149,13 @@ 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 + Cisco Catalyst 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 + THIRD_PARTY_DEVICE - This category encompasses devices from vendors other than Cisco or Meraki. Cisco Catalyst 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. + environments efficiently using Cisco Catalyst 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 @@ -175,10 +175,6 @@ 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 @@ -247,9 +243,6 @@ with a default value of False. type: bool default: False - 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. CREDENTIALDETAILS - Used for exporting device credentials details like snpm credntials, device crdentails etc. @@ -263,6 +256,25 @@ description: Location of the sites allocated for the APs type: list elements: str + provision_wired_device: + description: A list of dictionaries containing the IP address of wired devices and the site name where they will be provisioned. + type: list + elements: dict + suboptions: + device_ip: + description: The IP address of the wired device. + type: str + site_name: + description: The complete name of the site where the wired device will be provisioned(For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). + type: str + resync_retry_count: + description: The total number of retries to check whether the device has come to a managed state for provisioning. + type: int + default: 200 + resync_retry_interval: + description: The interval (in seconds) at which the system will check the device status during provisioning. + type: int + default: 2 dynamic_interfaces: description: Interface details of the wireless device type: list @@ -350,7 +362,6 @@ snmp_username: v3Public snmp_version: v3 type: NETWORK_DEVICE - device_added: True username: cisco - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device @@ -380,7 +391,6 @@ snmp_username: v3Public compute_device: True username: cisco - device_added: True type: "COMPUTE_DEVICE" - name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. @@ -397,7 +407,6 @@ state: merged config: - http_password: "test" - device_added: True type: "MERAKI_DASHBOARD" - name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. @@ -417,7 +426,6 @@ http_username: "testuser" http_password: "test" http_port: "443" - device_added: True type: "FIREPOWER_MANAGEMENT_SYSTEM" - name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. @@ -442,7 +450,6 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: v3Public - device_added: True type: "THIRD_PARTY_DEVICE" - name: Update device details or credentails in Inventory @@ -500,9 +507,15 @@ dnac_log: False state: merged config: - - ip_address_list: ["1.1.1.1", "2.2.2.2"] - provision_wired_device: + - provision_wired_device: + - device_ip: "1.1.1.1" site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" + resync_retry_count: 200 + resync_interval: 2 + - device_ip: "2.2.2.2" + site_name: "Global/USA/San Francisco/BGL_18/floor_test" + resync_retry_count: 200 + resync_retry_interval: 2 - name: Associate Wireless Devices to site and Provisioned it in Inventory cisco.dnac.inventory_intent: @@ -705,6 +718,7 @@ pyzipper = None import csv +import time from datetime import datetime from io import BytesIO, StringIO from ansible.module_utils.basic import AnsibleModule @@ -769,7 +783,6 @@ def validate_input(self): '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'}, @@ -797,7 +810,13 @@ def validate_input(self): 'operation_enum': {'type': 'str'}, 'parameters': {'type': 'list', 'elements': 'str'}, }, - 'provision_wired_device': {'type': 'dict'}, + 'provision_wired_device': { + 'type': 'list', + 'device_ip': {'type': 'str'}, + 'site_name': {'type': 'str'}, + 'resync_retry_count': {'default': 200, 'type': 'int'}, + 'resync_retry_interval': {'default': 2, 'type': 'int'}, + }, 'provision_wireless_device': { 'type': 'list', 'site_name': {'type': 'str'}, @@ -1559,60 +1578,62 @@ def provisioned_wired_device(self): 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. + It retrieves the site name and IP addresses of the devices from the list of configuration, + attempts to provision each device with site, and monitors the provisioning process. """ - site_name = self.config[0]['provision_wired_device']['site_name'] - device_in_dnac = self.device_exists_in_dnac() - 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_dnac: - input_device_ips.remove(device_ip) - - device_type = "Wired" + provision_wired_list = self.config[0]['provision_wired_device'] + total_devices_to_provisioned = len(provision_wired_list) + device_ip_list = [] 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 prov_dict in provision_wired_list: + managed_flag = False + device_ip = prov_dict['device_ip'] + device_ip_list.append(device_ip) + site_name = prov_dict['site_name'] + device_type = "Wired" + resync_retry_count = prov_dict.get("resync_retry_count", 200) + # This resync retry interval will be in seconds which will check device status at given interval + resync_retry_interval = prov_dict.get("resync_retry_interval", 2) + + if not site_name and not device_ip: + 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 - for device_ip in input_device_ips: - try: - provision_wired_params['deviceManagementIpAddress'] = device_ip - count = 1 - managed_flag = True + provision_wired_params = { + 'deviceManagementIpAddress': device_ip, + 'siteNameHierarchy': site_name + } - # 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") + # Check till device comes into managed state + while resync_retry_count: + 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") + ): + managed_flag = True + break + if response.get('collectionStatus') == "Partial Collection Failure" or response.get('collectionStatus') == "Could Not Synchronize": + managed_flag = False + break - 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 + time.sleep(resync_retry_interval) + resync_retry_count = resync_retry_count - 1 - 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 + 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 + try: response = self.dnac._exec( family="sda", function='provision_wired_device', @@ -1650,9 +1671,9 @@ def provisioned_wired_device(self): 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): + if already_provision_count == total_devices_to_provisioned: + self.handle_all_already_provisioned(device_ip_list, device_type) + elif provision_count == total_devices_to_provisioned: self.handle_all_provisioned(device_type) elif provision_count == 0: self.handle_all_failed_provision(device_type) @@ -1963,16 +1984,28 @@ def get_have(self, config): # Get the list of device that are present in Cisco Catalyst Center device_in_dnac = self.device_exists_in_dnac() - device_not_in_dnac = [] + device_not_in_dnac, devices_in_playbook = [], [] for ip in want_device: + devices_in_playbook.append(ip) if ip not in device_in_dnac: device_not_in_dnac.append(ip) + if self.config[0].get('provision_wired_device'): + provision_wired_list = self.config[0].get('provision_wired_device') + + for prov_dict in provision_wired_list: + device_ip_address = prov_dict['device_ip'] + if device_ip_address not in want_device: + devices_in_playbook.append(device_ip_address) + if device_ip_address not in device_in_dnac: + device_not_in_dnac.append(device_ip_address) + self.log("Device(s) {0} exists in Cisco Catalyst Center".format(str(device_in_dnac)), "INFO") have["want_device"] = want_device have["device_in_dnac"] = device_in_dnac have["device_not_in_dnac"] = device_not_in_dnac + have["devices_in_playbook"] = devices_in_playbook self.have = have self.log("Current State (have): {0}".format(str(self.have)), "INFO") @@ -2723,7 +2756,6 @@ def get_diff_merged(self, config): devices_to_add = self.have["device_not_in_dnac"] 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) @@ -2956,10 +2988,19 @@ def get_diff_merged(self, config): self.log(error_message, "ERROR") raise Exception(error_message) - # If we want to add device in inventory - if device_added: - config['ip_address_list'] = devices_to_add + config['ip_address_list'] = devices_to_add + + if not config['ip_address_list']: + self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + + # To add the devices in inventory + if config['ip_address_list']: device_params = self.want.get("device_params") + device_params['ipAddress'] = config['ip_address_list'] + if not device_params['snmpMode']: device_params['snmpMode'] = "AUTHPRIV" @@ -3263,21 +3304,19 @@ def verify_diff_merged(self, config): self.log("Desired State (want): {0}".format(str(self.want)), "INFO") devices_to_add = self.have["device_not_in_dnac"] - 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 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(self.have['devices_in_playbook'])) + 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 @@ -3341,16 +3380,20 @@ def verify_diff_merged(self, config): device role update task may not have executed successfully.""", "INFO") if self.config[0].get('provision_wired_device'): + provision_wired_list = self.config[0].get('provision_wired_device') provision_wired_flag = True + provision_device_list = [] - for device_ip in device_ips: + for prov_dict in provision_wired_list: + device_ip = prov_dict['device_ip'] + provision_device_list.append(device_ip) 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) + msg = "Wired devices {0} get provisioned and verified successfully.".format(provision_device_list) self.log(msg, "INFO") else: self.log("""Mismatch between playbook's input and Cisco Catalyst Center detected, indicating that diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index b354fe875e..7327c79c34 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -149,13 +149,13 @@ 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 + Cisco Catalyst 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 + THIRD_PARTY_DEVICE - This category encompasses devices from vendors other than Cisco or Meraki. Cisco Catalyst 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. + environments efficiently using Cisco Catalyst 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 @@ -175,10 +175,6 @@ 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 @@ -248,9 +244,6 @@ with a default value of False. type: bool default: False - 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. CREDENTIALDETAILS - Used for exporting device credentials details like snpm credntials, device crdentails etc. @@ -264,6 +257,25 @@ description: Location of the sites allocated for the APs type: list elements: str + provision_wired_device: + description: A list of dictionaries containing the IP address of wired devices and the site name where they will be provisioned. + type: list + elements: dict + suboptions: + device_ip: + description: The IP address of the wired device. + type: str + site_name: + description: The complete name of the site where the wired device will be provisioned(For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). + type: str + resync_retry_count: + description: The total number of retries to check whether the device has come to a managed state for provisioning. + type: int + default: 200 + resync_retry_interval: + description: The interval (in seconds) at which the system will check the device status during provisioning. + type: int + default: 2 dynamic_interfaces: description: Interface details of the wireless device type: list @@ -351,7 +363,6 @@ snmp_username: v3Public snmp_version: v3 type: NETWORK_DEVICE - device_added: True username: cisco - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device @@ -381,7 +392,6 @@ snmp_username: v3Public compute_device: True username: cisco - device_added: True type: "COMPUTE_DEVICE" - name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. @@ -398,7 +408,6 @@ state: merged config: - http_password: "test" - device_added: True type: "MERAKI_DASHBOARD" - name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. @@ -418,7 +427,6 @@ http_username: "testuser" http_password: "test" http_port: "443" - device_added: True type: "FIREPOWER_MANAGEMENT_SYSTEM" - name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. @@ -443,7 +451,6 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: v3Public - device_added: True type: "THIRD_PARTY_DEVICE" - name: Update device details or credentails in Inventory @@ -501,9 +508,15 @@ dnac_log: False state: merged config: - - ip_address_list: ["1.1.1.1", "2.2.2.2"] - provision_wired_device: + - provision_wired_device: + - device_ip: "1.1.1.1" site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" + resync_retry_count: 200 + resync_interval: 2 + - device_ip: "2.2.2.2" + site_name: "Global/USA/San Francisco/BGL_18/floor_test" + resync_retry_count: 200 + resync_retry_interval: 2 - name: Associate Wireless Devices to site and Provisioned it in Inventory cisco.dnac.inventory_workflow_manager: @@ -705,6 +718,7 @@ pyzipper = None import csv +import time from datetime import datetime from io import BytesIO, StringIO from ansible.module_utils.basic import AnsibleModule @@ -769,7 +783,6 @@ def validate_input(self): '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'}, @@ -797,7 +810,13 @@ def validate_input(self): 'operation_enum': {'type': 'str'}, 'parameters': {'type': 'list', 'elements': 'str'}, }, - 'provision_wired_device': {'type': 'dict'}, + 'provision_wired_device': { + 'type': 'list', + 'device_ip': {'type': 'str'}, + 'site_name': {'type': 'str'}, + 'resync_retry_count': {'default': 200, 'type': 'int'}, + 'resync_retry_interval': {'default': 2, 'type': 'int'}, + }, 'provision_wireless_device': { 'type': 'list', 'site_name': {'type': 'str'}, @@ -1558,60 +1577,62 @@ def provisioned_wired_device(self): 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. + It retrieves the site name and IP addresses of the devices from the list of configuration, + attempts to provision each device with site, 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_wired_list = self.config[0]['provision_wired_device'] + total_devices_to_provisioned = len(provision_wired_list) + device_ip_list = [] 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 prov_dict in provision_wired_list: + managed_flag = False + device_ip = prov_dict['device_ip'] + device_ip_list.append(device_ip) + site_name = prov_dict['site_name'] + device_type = "Wired" + resync_retry_count = prov_dict.get("resync_retry_count", 200) + # This resync retry interval will be in seconds which will check device status at given interval + resync_retry_interval = prov_dict.get("resync_retry_interval", 2) + + if not site_name and not device_ip: + 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 - for device_ip in input_device_ips: - try: - provision_wired_params['deviceManagementIpAddress'] = device_ip - count = 1 - managed_flag = True + provision_wired_params = { + 'deviceManagementIpAddress': device_ip, + 'siteNameHierarchy': site_name + } - # 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") + # Check till device comes into managed state + while resync_retry_count: + 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") + ): + managed_flag = True + break + if response.get('collectionStatus') == "Partial Collection Failure" or response.get('collectionStatus') == "Could Not Synchronize": + managed_flag = False + break - 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 + time.sleep(resync_retry_interval) + resync_retry_count = resync_retry_count - 1 - 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 + 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 + try: response = self.dnac._exec( family="sda", function='provision_wired_device', @@ -1649,9 +1670,9 @@ def provisioned_wired_device(self): 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): + if already_provision_count == total_devices_to_provisioned: + self.handle_all_already_provisioned(device_ip_list, device_type) + elif provision_count == total_devices_to_provisioned: self.handle_all_provisioned(device_type) elif provision_count == 0: self.handle_all_failed_provision(device_type) @@ -1963,16 +1984,28 @@ def get_have(self, config): # Get the list of device that are present in Cisco Catalyst Center device_in_ccc = self.device_exists_in_ccc() - device_not_in_ccc = [] + device_not_in_ccc, devices_in_playbook = [], [] for ip in want_device: + devices_in_playbook.append(ip) if ip not in device_in_ccc: device_not_in_ccc.append(ip) + if self.config[0].get('provision_wired_device'): + provision_wired_list = self.config[0].get('provision_wired_device') + + for prov_dict in provision_wired_list: + device_ip_address = prov_dict['device_ip'] + if device_ip_address not in want_device: + devices_in_playbook.append(device_ip_address) + if device_ip_address not in device_in_ccc: + device_not_in_ccc.append(device_ip_address) + 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 + have["devices_in_playbook"] = devices_in_playbook self.have = have self.log("Current State (have): {0}".format(str(self.have)), "INFO") @@ -2720,11 +2753,9 @@ def get_diff_merged(self, config): 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) @@ -2959,10 +2990,19 @@ def get_diff_merged(self, config): self.log(error_message, "ERROR") raise Exception(error_message) - # If we want to add device in inventory - if device_added: - config['ip_address_list'] = devices_to_add + config['ip_address_list'] = devices_to_add + + if not config['ip_address_list']: + self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + + # To add the devices in inventory + if config['ip_address_list']: device_params = self.want.get("device_params") + device_params['ipAddress'] = config['ip_address_list'] + if not device_params['snmpMode']: device_params['snmpMode'] = "AUTHPRIV" @@ -3266,21 +3306,19 @@ def verify_diff_merged(self, config): 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 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(self.have['devices_in_playbook'])) + 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 @@ -3344,16 +3382,20 @@ def verify_diff_merged(self, config): device role update task may not have executed successfully.""", "INFO") if self.config[0].get('provision_wired_device'): + provision_wired_list = self.config[0].get('provision_wired_device') provision_wired_flag = True + provision_device_list = [] - for device_ip in device_ips: + for prov_dict in provision_wired_list: + device_ip = prov_dict['device_ip'] + provision_device_list.append(device_ip) 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) + msg = "Wired devices {0} get provisioned and verified successfully.".format(provision_device_list) self.log(msg, "INFO") else: self.log("""Mismatch between playbook's input and Cisco Catalyst Center detected, indicating that From a2763cfc9a288e25e094d928371545a78c397771 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 23 Feb 2024 20:14:11 +0530 Subject: [PATCH 07/44] remove duplicate key site_name from playbook --- playbooks/inventory_workflow_manager.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index 0c284b83c7..ce309a068a 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -58,7 +58,6 @@ site_name: "Global/USA/San Francisco/BGL_18/floor_test" resync_retry_count: 200 resync_retry_interval: 2 - site_name: "{{item.site_name}}" update_interface_details: description: "{{item.update_interface_details.description}}" interface_name: "{{item.interface_name}}" From 007593b2e1fa3a3643124f21867834ab28b6a09e Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Sat, 24 Feb 2024 13:33:01 +0530 Subject: [PATCH 08/44] make device add function in the beginning and address review comments --- plugins/modules/inventory_intent.py | 190 ++++++++++-------- plugins/modules/inventory_workflow_manager.py | 189 +++++++++-------- 2 files changed, 205 insertions(+), 174 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 2a02fa1e22..cd410d0f98 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -257,22 +257,27 @@ type: list elements: str provision_wired_device: - description: A list of dictionaries containing the IP address of wired devices and the site name where they will be provisioned. + description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wired device and + the name of the site where the device will be provisioned. type: list elements: dict suboptions: device_ip: - description: The IP address of the wired device. + description: Specifies the IP address of the wired device. This is a string value that should be in the format of + standard IPv4 or IPv6 addresses. type: str site_name: - description: The complete name of the site where the wired device will be provisioned(For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). + description: Indicates the exact location where the wired device will be provisioned. This is a string value that should + represent the complete hierarchical path of the site (For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). type: str resync_retry_count: - description: The total number of retries to check whether the device has come to a managed state for provisioning. + description: Determines the total number of retry attempts for checking if the device has reached a managed state during + the provisioning process. If unspecified, the default value is set to 200 retries. type: int default: 200 resync_retry_interval: - description: The interval (in seconds) at which the system will check the device status during provisioning. + description: Sets the interval, in seconds, at which the system will recheck the device status throughout the provisioning + process. If unspecified, the system will check the device status every 2 seconds by default. type: int default: 2 dynamic_interfaces: @@ -1619,9 +1624,17 @@ def provisioned_wired_device(self): and response.get('collectionStatus') == "Managed" and response.get("hostname") ): + msg = """Device '{0}' comes to managed state and ready for provisioning with the resync_retry_count + '{1}' left having resync interval of {2} seconds""".format(device_ip, resync_retry_count, resync_retry_interval) + self.log(msg, "INFO") managed_flag = True break + if response.get('collectionStatus') == "Partial Collection Failure" or response.get('collectionStatus') == "Could Not Synchronize": + device_status = response.get('collectionStatus') + msg = """Device '{0}' comes to '{1}' state and never goes for provisioning with the resync_retry_count + '{2}' left having resync interval of {3} seconds""".format(device_ip, device_status, resync_retry_count, resync_retry_interval) + self.log(msg, "INFO") managed_flag = False break @@ -2764,6 +2777,88 @@ def get_diff_merged(self, config): if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": config['http_port'] = self.config[0].get("http_port", "443") + config['ip_address_list'] = devices_to_add + + if not config['ip_address_list']: + self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + + # To add the devices in inventory + if config['ip_address_list']: + device_params = self.want.get("device_params") + device_params['ipAddress'] = config['ip_address_list'] + + if not device_params['snmpMode']: + device_params['snmpMode'] = "AUTHPRIV" + + if not device_params['cliTransport']: + device_params['cliTransport'] = "ssh" + + if not device_params['snmpPrivProtocol']: + device_params['snmpPrivProtocol'] = "AES128" + + if device_params['snmpPrivProtocol'] == "AES192": + device_params['snmpPrivProtocol'] = "CISCOAES192" + elif device_params['snmpPrivProtocol'] == "AES256": + device_params['snmpPrivProtocol'] = "CISCOAES256" + + 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_list"))) + 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 device_updated: device_to_update = self.get_device_ips_from_config_priority() # First check if device present in Cisco Catalyst Center or not @@ -2916,9 +3011,11 @@ def get_diff_merged(self, config): self.log(error_message, "ERROR") raise Exception(error_message) + # Update list of interface details on specific or list of devices. if self.config[0].get('update_interface_details'): self.update_interface_detail_of_device(device_to_update).check_return_status() + # Update the role of devices having the role source as Manual if self.config[0].get('update_device_role'): for device_ip in device_to_update: device_id = self.get_device_ids([device_ip]) @@ -2988,88 +3085,7 @@ def get_diff_merged(self, config): self.log(error_message, "ERROR") raise Exception(error_message) - config['ip_address_list'] = devices_to_add - - if not config['ip_address_list']: - self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) - self.log(self.msg, "INFO") - self.result['changed'] = False - self.result['response'] = self.msg - - # To add the devices in inventory - if config['ip_address_list']: - device_params = self.want.get("device_params") - device_params['ipAddress'] = config['ip_address_list'] - - if not device_params['snmpMode']: - device_params['snmpMode'] = "AUTHPRIV" - - if not device_params['cliTransport']: - device_params['cliTransport'] = "ssh" - - if not device_params['snmpPrivProtocol']: - device_params['snmpPrivProtocol'] = "AES128" - - if device_params['snmpPrivProtocol'] == "AES192": - device_params['snmpPrivProtocol'] = "CISCOAES192" - elif device_params['snmpPrivProtocol'] == "AES256": - device_params['snmpPrivProtocol'] = "CISCOAES256" - - 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_list"))) - 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 User defined field(UDF) not present then create it and add multiple udf to specific or list of devices if self.config[0].get('add_user_defined_field'): udf_field_list = self.config[0].get('add_user_defined_field') diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 7327c79c34..501032ca44 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -258,22 +258,27 @@ type: list elements: str provision_wired_device: - description: A list of dictionaries containing the IP address of wired devices and the site name where they will be provisioned. + description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wired device and + the name of the site where the device will be provisioned. type: list elements: dict suboptions: device_ip: - description: The IP address of the wired device. + description: Specifies the IP address of the wired device. This is a string value that should be in the format of + standard IPv4 or IPv6 addresses. type: str site_name: - description: The complete name of the site where the wired device will be provisioned(For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). + description: Indicates the exact location where the wired device will be provisioned. This is a string value that should + represent the complete hierarchical path of the site (For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). type: str resync_retry_count: - description: The total number of retries to check whether the device has come to a managed state for provisioning. + description: Determines the total number of retry attempts for checking if the device has reached a managed state during + the provisioning process. If unspecified, the default value is set to 200 retries. type: int default: 200 resync_retry_interval: - description: The interval (in seconds) at which the system will check the device status during provisioning. + description: Sets the interval, in seconds, at which the system will recheck the device status throughout the provisioning + process. If unspecified, the system will check the device status every 2 seconds by default. type: int default: 2 dynamic_interfaces: @@ -1618,9 +1623,16 @@ def provisioned_wired_device(self): and response.get('collectionStatus') == "Managed" and response.get("hostname") ): + msg = """Device '{0}' comes to managed state and ready for provisioning with the resync_retry_count + '{1}' left having resync interval of {2} seconds""".format(device_ip, resync_retry_count, resync_retry_interval) + self.log(msg, "INFO") managed_flag = True break if response.get('collectionStatus') == "Partial Collection Failure" or response.get('collectionStatus') == "Could Not Synchronize": + device_status = response.get('collectionStatus') + msg = """Device '{0}' comes to '{1}' state and never goes for provisioning with the resync_retry_count + '{2}' left having resync interval of {3} seconds""".format(device_ip, device_status, resync_retry_count, resync_retry_interval) + self.log(msg, "INFO") managed_flag = False break @@ -2764,6 +2776,88 @@ def get_diff_merged(self, config): if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": config['http_port'] = self.config[0].get("http_port", "443") + config['ip_address_list'] = devices_to_add + + if not config['ip_address_list']: + self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + + # To add the devices in inventory + if config['ip_address_list']: + device_params = self.want.get("device_params") + device_params['ipAddress'] = config['ip_address_list'] + + if not device_params['snmpMode']: + device_params['snmpMode'] = "AUTHPRIV" + + if not device_params['cliTransport']: + device_params['cliTransport'] = "ssh" + + if not device_params['snmpPrivProtocol']: + device_params['snmpPrivProtocol'] = "AES128" + + if device_params['snmpPrivProtocol'] == "AES192": + device_params['snmpPrivProtocol'] = "CISCOAES192" + elif device_params['snmpPrivProtocol'] == "AES256": + device_params['snmpPrivProtocol'] = "CISCOAES256" + + 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_list"))) + 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 device_updated: device_to_update = self.get_device_ips_from_config_priority() # First check if device present in Cisco Catalyst Center or not @@ -2918,9 +3012,11 @@ def get_diff_merged(self, config): self.log(error_message, "ERROR") raise Exception(error_message) + # Update list of interface details on specific or list of devices. if self.config[0].get('update_interface_details'): self.update_interface_detail_of_device(device_to_update).check_return_status() + # Update the role of devices having the role source as Manual if self.config[0].get('update_device_role'): for device_ip in device_to_update: device_id = self.get_device_ids([device_ip]) @@ -2990,88 +3086,7 @@ def get_diff_merged(self, config): self.log(error_message, "ERROR") raise Exception(error_message) - config['ip_address_list'] = devices_to_add - - if not config['ip_address_list']: - self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) - self.log(self.msg, "INFO") - self.result['changed'] = False - self.result['response'] = self.msg - - # To add the devices in inventory - if config['ip_address_list']: - device_params = self.want.get("device_params") - device_params['ipAddress'] = config['ip_address_list'] - - if not device_params['snmpMode']: - device_params['snmpMode'] = "AUTHPRIV" - - if not device_params['cliTransport']: - device_params['cliTransport'] = "ssh" - - if not device_params['snmpPrivProtocol']: - device_params['snmpPrivProtocol'] = "AES128" - - if device_params['snmpPrivProtocol'] == "AES192": - device_params['snmpPrivProtocol'] = "CISCOAES192" - elif device_params['snmpPrivProtocol'] == "AES256": - device_params['snmpPrivProtocol'] = "CISCOAES256" - - 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_list"))) - 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 User defined field(UDF) not present then create it and add multiple udf to specific or list of devices if self.config[0].get('add_user_defined_field'): udf_field_list = self.config[0].get('add_user_defined_field') From 19b58b418286ee178d5fbaf5acfa16d1b6561a26 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Sat, 24 Feb 2024 16:30:59 +0530 Subject: [PATCH 09/44] make check in the if condition while provisioning wired device --- plugins/modules/inventory_intent.py | 2 +- plugins/modules/inventory_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index cd410d0f98..49567a6d93 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1602,7 +1602,7 @@ def provisioned_wired_device(self): # This resync retry interval will be in seconds which will check device status at given interval resync_retry_interval = prov_dict.get("resync_retry_interval", 2) - if not site_name and not device_ip: + if not site_name or not device_ip: self.status = "failed" self.msg = "Site/Devices are required for Provisioning of Wired Devices." self.log(self.msg, "ERROR") diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 501032ca44..5c2f60afda 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -1601,7 +1601,7 @@ def provisioned_wired_device(self): # This resync retry interval will be in seconds which will check device status at given interval resync_retry_interval = prov_dict.get("resync_retry_interval", 2) - if not site_name and not device_ip: + if not site_name or not device_ip: self.status = "failed" self.msg = "Site/Devices are required for Provisioning of Wired Devices." self.log(self.msg, "ERROR") From e54401af83321b606d39af0a5a8b8fe20ca9a2ed Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 27 Feb 2024 11:20:55 +0530 Subject: [PATCH 10/44] Write the API for Wireless Device Provisioning with the new discussed format, also new API for Re-provisioned wired device in Inventory, Add the support for Adding/Updating device with snmp_version - v2 as well, update documentation, examples and playbook for same. --- playbooks/inventory_workflow_manager.yml | 30 ++ plugins/modules/inventory_intent.py | 488 +++++++++++++----- plugins/modules/inventory_workflow_manager.py | 478 ++++++++++++----- 3 files changed, 752 insertions(+), 244 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index ce309a068a..6a54a24df3 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -58,6 +58,36 @@ site_name: "Global/USA/San Francisco/BGL_18/floor_test" resync_retry_count: 200 resync_retry_interval: 2 + provision_wireless_device: + - device_ip: "1.1.1.1" + site_name: "Global/USA/BGL_18/floor_pnp" + managed_ap_locations: ["Global/USA/BGL_18/floor_pnp", "Global/USA/BGL_18/floor_test"] + dynamic_interfaces: + - 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" + resync_retry_count: 200 + resync_retry_interval: 2 + - device_ip: "2.2.2.2" + site_name: "Global/USA/BGL_18/floor_test" + managed_ap_locations: ["Global/USA/BGL_19/floor_pnp", "Global/USA/BGL_19/floor_test"] + dynamic_interfaces: + - interface_ip_address: 32.31.12.23 + interface_netmask_in_cidr: 26 + interface_gateway: "gateway_test" + lag_or_port_number: 33 + vlan_id: 78 + interface_name: "etherenet1/1" + resync_retry_count: 200 + resync_retry_interval: 2 + reprovision_wired_device: + - device_ip: "1.1.1.1" + site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" + - device_ip: "2.2.2.2" + site_name: "Global/USA/San Francisco/BGL_18/floor_test" update_interface_details: description: "{{item.update_interface_details.description}}" interface_name: "{{item.interface_name}}" diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 49567a6d93..971b84ec92 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -92,6 +92,9 @@ 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. + NETCONF with user privilege 15 is mandatory for enabling Wireless Services on Wireless capable devices such as Catalyst 9000 series + Switches and C9800 Series Wireless Controllers. The NETCONF credentials are required to connect to C9800 Series Wireless Controllers + as the majority of data collection is done using NETCONF for these Devices. type: str username: description: Username for accessing the device. Required for Adding Network Device. @@ -141,9 +144,14 @@ description: SNMP username required for adding network, compute, and third-party devices. type: str snmp_version: - description: Device's snmp Version. + description: It is a standard protocol used for managing and monitoring network devices. + v2 - In this communication between the SNMP manager (such as Cisco Catalyst) and the managed devices + (such as routers, switches, or access points) is based on community strings.Community strings serve + as form of authentication and they are transmitted in clear text, providing no encryption. + v3 - It is the most secure version of SNMP, providing authentication, integrity, and encryption features. + It allows for the use of usernames, authentication passwords, and encryption keys, providing stronger + security compared to v2. type: str - 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 @@ -252,10 +260,6 @@ description: List of device parameters that needs to be exported to file. type: list elements: str - managed_ap_locations: - description: Location of the sites allocated for the APs - type: list - elements: str provision_wired_device: description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wired device and the name of the site where the device will be provisioned. @@ -280,30 +284,73 @@ process. If unspecified, the system will check the device status every 2 seconds by default. type: int default: 2 - dynamic_interfaces: - description: Interface details of the wireless device + reprovision_wired_device: + description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wired device and + the name of the site where the device will be re-provisioned. type: list elements: dict suboptions: - interface_ip_address: - description: Ip Address allocated to the interface + device_ip: + description: Specifies the IP address of the wired device. This is a string value that should be in the format of + standard IPv4 or IPv6 addresses. 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. + site_name: + description: Indicates the exact location where the wired device will be provisioned. This is a string value that should + represent the complete hierarchical path of the site (For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). + type: str + provision_wireless_device: + description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wireless device and + the name of the site where the device will be provisioned along with dynamic interface details. + type: list + elements: dict + suboptions: + device_ip: + description: Specifies the IP address of the wirelesss device. This is a string value that should be in the format of + standard IPv4 or IPv6 addresses. + type: str + site_name: + description: Indicates the exact location where the wired device will be provisioned. This is a string value that should + represent the complete hierarchical path of the site (For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). type: str - lag_or_port_number: - description: The Link Aggregation Group (LAG) number or port number assigned to the interface. + managed_ap_locations: + description: Location of the sites allocated for the APs (For example, ["Global/USA/San Francisco/BGL_18/floor_test", + "Global/USA/San Francisco/BGL_18/floor_check"]) + 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 + resync_retry_count: + description: Determines the total number of retry attempts for checking if the device has reached a managed state during + the provisioning process. If unspecified, the default value is set to 200 retries. type: int - vlan_id: - description: The VLAN (Virtual Local Area Network) ID associated with the network interface. + default: 200 + resync_retry_interval: + description: Sets the interval, in seconds, at which the system will recheck the device status throughout the provisioning + process. If unspecified, the system will check the device status every 2 seconds by default. type: int - interface_name: - description: Name of the interface. - type: str + default: 2 requirements: - dnacentersdk >= 2.5.5 @@ -522,6 +569,25 @@ resync_retry_count: 200 resync_retry_interval: 2 +- name: Re-Provisioned Wired Devices to site in Inventory + cisco.dnac.inventory_intent: + 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: + - reprovision_wired_device: + - device_ip: "1.1.1.1" + site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" + - device_ip: "2.2.2.2" + site_name: "Global/USA/San Francisco/BGL_18/floor_test" + - name: Associate Wireless Devices to site and Provisioned it in Inventory cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -535,17 +601,31 @@ dnac_log: False state: merged config: - - ip_address_list: ["1.1.1.1", "2.2.2.2"] - provision_wireless_device: - 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: 23.23.21.12 - interface_netmask_in_cidr: 24 - interface_gateway: "gateway" - lag_or_port_number: 12 - vlan_id: 99 - interface_name: "etherenet0/0" + - provision_wireless_device: + - device_ip: "1.1.1.1" + site_name: "Global/USA/BGL_18/floor_pnp" + managed_ap_locations: ["Global/USA/BGL_18/floor_pnp", "Global/USA/BGL_18/floor_test"] + dynamic_interfaces: + - 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" + resync_retry_count: 200 + resync_retry_interval: 2 + - device_ip: "2.2.2.2" + site_name: "Global/USA/BGL_18/floor_test" + managed_ap_locations: ["Global/USA/BGL_19/floor_pnp", "Global/USA/BGL_19/floor_test"] + dynamic_interfaces: + - interface_ip_address: 32.31.12.23 + interface_netmask_in_cidr: 26 + interface_gateway: "gateway_test" + lag_or_port_number: 33 + vlan_id: 78 + interface_name: "etherenet1/1" + resync_retry_count: 200 + resync_retry_interval: 2 - name: Update Device Role with IP Address cisco.dnac.inventory_intent: @@ -784,7 +864,7 @@ def validate_input(self): 'snmp_retry': {'default': 3, 'type': 'int'}, 'snmp_timeout': {'default': 5, 'type': 'int'}, 'snmp_username': {'type': 'str'}, - 'snmp_version': {'default': "v3", 'type': 'str'}, + 'snmp_version': {'type': 'str'}, 'update_mgmt_ipaddresslist': {'type': 'list', 'elements': 'dict'}, 'username': {'type': 'str'}, 'update_device_role': {'type': 'dict'}, @@ -822,8 +902,14 @@ def validate_input(self): 'resync_retry_count': {'default': 200, 'type': 'int'}, 'resync_retry_interval': {'default': 2, 'type': 'int'}, }, + 'reprovision_wired_device': { + 'type': 'list', + 'device_ip': {'type': 'str'}, + 'site_name': {'type': 'str'}, + }, 'provision_wireless_device': { 'type': 'list', + 'device_ip': {'type': 'str'}, 'site_name': {'type': 'str'}, 'managed_ap_locations': {'type': 'list', 'elements': 'str'}, 'dynamic_interfaces': { @@ -835,6 +921,8 @@ def validate_input(self): 'vlan_id': {'type': 'int'}, 'interface_name': {'type': 'str'}, }, + 'resync_retry_count': {'default': 200, 'type': 'int'}, + 'resync_retry_interval': {'default': 2, 'type': 'int'}, } } @@ -1604,7 +1692,7 @@ def provisioned_wired_device(self): if not site_name or not device_ip: self.status = "failed" - self.msg = "Site/Devices are required for Provisioning of Wired Devices." + self.msg = "Site and Device IP are required for Provisioning of Wired Devices." self.log(self.msg, "ERROR") self.result['response'] = self.msg return self @@ -1665,7 +1753,6 @@ def provisioned_wired_device(self): 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) @@ -1695,12 +1782,102 @@ def provisioned_wired_device(self): return self - def get_wireless_param(self, device_ip): + def reprovisioned_wired_device(self): + """ + Re-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 re-provision wired devices in Cisco Catalyst Center based on the configuration provided. + It retrieves the site name and IP addresses of the devices from the list of configuration, + attempts to provision each device with site, and monitors the provisioning process. + """ + + reprovision_wired_list = self.config[0]['reprovision_wired_device'] + total_devices_to_reprovisioned = len(reprovision_wired_list) + device_in_dnac = self.device_exists_in_dnac() + device_ip_list = [] + provision_count, already_provision_count = 0, 0 + + for prov_dict in reprovision_wired_list: + device_ip = prov_dict['device_ip'] + device_ip_list.append(device_ip) + site_name = prov_dict['site_name'] + device_type = "Wired" + + if device_ip not in device_in_dnac: + self.msg = "Device '{0}' not present in Cisco Catalyst Center so cannot re-provisioned it.".format(device_ip) + self.log(self.msg, "WARNING") + continue + + if not site_name or not device_ip: + self.status = "failed" + self.msg = "Site/Devices are required for Re-Provisioning of Wired Devices." + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + reprovision_wired_params = { + 'deviceManagementIpAddress': device_ip, + 'siteNameHierarchy': site_name + } + + try: + response = self.dnac._exec( + family="sda", + function='re_provision_wired_device', + op_modifies=True, + params=reprovision_wired_params, + ) + + if response.get("status") == "failed": + description = response.get("description") + error_msg = "Cannot do Re-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("data") + + if 'processcfs_complete=true' 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 == total_devices_to_reprovisioned: + self.handle_all_already_provisioned(device_ip_list, device_type) + elif provision_count == total_devices_to_reprovisioned: + 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, prov_dict): """ 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. + prov_dict (dict): A dictionary containing configuration parameters for wireless provisioning. Returns: wireless_param (list of dict): A list containing a dictionary with wireless provisioning parameters. Description: @@ -1712,50 +1889,59 @@ def get_wireless_param(self, device_ip): 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'], - } - ] + try: + device_ip_address = prov_dict['device_ip'] + site_name = prov_dict['site_name'] - 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 = [ + { + 'site': site_name, + 'managedAPLocations': prov_dict['managed_ap_locations'], + } + ] - wireless_param[0]["dynamicInterfaces"] = [] + 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 - 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) + wireless_param[0]["dynamicInterfaces"] = [] - 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 + for interface in prov_dict.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 = 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") + response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"ip_address": device_ip_address} + ) + + 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 the parameters required for Wireless Provisioning", "DEBUG") + + except Exception as e: + self.msg = """An exception occured while fetching the details for wireless provisioning of + device '{0}' due to - {1}""".format(device_ip_address, str(e)) + self.log(self.msg, "ERROR") return self @@ -1800,12 +1986,11 @@ def get_site_type(self, site_name): return site_type - def provisioned_wireless_devices(self, device_ips): + def provisioned_wireless_devices(self): """ 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: @@ -1817,25 +2002,23 @@ def provisioned_wireless_devices(self, device_ips): provision_count, already_provision_count = 0, 0 device_type = "Wireless" + device_ip_list = [] + provision_wireless_list = self.config[0]['provision_wireless_device'] - device_in_dnac = self.device_exists_in_dnac() - 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_dnac: - input_device_ips.remove(device_ip) - - for device_ip in input_device_ips: + for prov_dict in provision_wireless_list: try: # Collect the device parameters from the playbook to perform wireless provisioing - self.get_wireless_param(device_ip).check_return_status() + self.get_wireless_param(prov_dict).check_return_status() + device_ip = prov_dict['device_ip'] + device_ip_list.append(device_ip) provisioning_params = self.wireless_param - count = 1 + resync_retry_count = prov_dict.get("resync_retry_count", 200) + # This resync retry interval will be in seconds which will check device status at given interval + resync_retry_interval = prov_dict.get("resync_retry_interval", 2) managed_flag = True # Check till device comes into managed state - while True: + while resync_retry_count: response = self.get_device_response(device_ip) self.log("Device is in {0} state waiting for Managed State.".format(response['managementState']), "DEBUG") @@ -1844,16 +2027,26 @@ def provisioned_wireless_devices(self, device_ips): and response.get('collectionStatus') == "Managed" and response.get("hostname") ): + msg = """Device '{0}' comes to managed state and ready for provisioning with the resync_retry_count + '{1}' left having resync interval of {2} seconds""".format(device_ip, resync_retry_count, resync_retry_interval) + self.log(msg, "INFO") + managed_flag = True break - count = count + 1 - if count > 200: + if response.get('collectionStatus') == "Partial Collection Failure" or response.get('collectionStatus') == "Could Not Synchronize": + device_status = response.get('collectionStatus') + msg = """Device '{0}' comes to '{1}' state and never goes for provisioning with the resync_retry_count + '{2}' left having resync interval of {3} seconds""".format(device_ip, device_status, resync_retry_count, resync_retry_interval) + self.log(msg, "INFO") managed_flag = False break + time.sleep(resync_retry_interval) + resync_retry_count = resync_retry_count - 1 + 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') + 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 @@ -1875,7 +2068,6 @@ def provisioned_wireless_devices(self, device_ips): 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 @@ -1894,9 +2086,9 @@ def provisioned_wireless_devices(self, device_ips): 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): + if already_provision_count == len(device_ip_list): + self.handle_all_already_provisioned(device_ip_list, device_type) + elif provision_count == len(device_ip_list): self.handle_all_provisioned(device_type) elif provision_count == 0: self.handle_all_failed_provision(device_type) @@ -1950,7 +2142,7 @@ def mandatory_parameter(self): device_type = self.config[0].get("type", "NETWORK_DEVICE") params_dict = { - "NETWORK_DEVICE": ["enable_password", "ip_address_list", "password", "snmp_username", "username"], + "NETWORK_DEVICE": ["enable_password", "ip_address_list", "password", "username"], "COMPUTE_DEVICE": ["ip_address_list", "http_username", "http_password", "http_port", "snmp_username"], "MERAKI_DASHBOARD": ["http_password"], "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address_list", "http_username", "http_password"], @@ -2014,6 +2206,16 @@ def get_have(self, config): if device_ip_address not in device_in_dnac: device_not_in_dnac.append(device_ip_address) + if self.config[0].get('provision_wireless_device'): + provision_wireless_list = self.config[0].get('provision_wireless_device') + + for prov_dict in provision_wireless_list: + device_ip_address = prov_dict['device_ip'] + if device_ip_address not in want_device and device_ip_address not in devices_in_playbook: + devices_in_playbook.append(device_ip_address) + if device_ip_address not in device_in_dnac and device_ip_address not in device_not_in_dnac: + device_not_in_dnac.append(device_ip_address) + self.log("Device(s) {0} exists in Cisco Catalyst Center".format(str(device_in_dnac)), "INFO") have["want_device"] = want_device have["device_in_dnac"] = device_in_dnac @@ -2787,31 +2989,40 @@ def get_diff_merged(self, config): # To add the devices in inventory if config['ip_address_list']: - device_params = self.want.get("device_params") - device_params['ipAddress'] = config['ip_address_list'] + input_params = self.want.get("device_params") + device_params = input_params.copy() + + if not device_params['snmpVersion']: + device_params['snmpVersion'] = "v3" - if not device_params['snmpMode']: - device_params['snmpMode'] = "AUTHPRIV" + device_params['ipAddress'] = config['ip_address_list'] + if device_params['snmpVersion'] == "v2": + params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] + for param in params_to_remove: + device_params.pop(param, None) + else: + if not device_params['snmpMode']: + device_params['snmpMode'] = "AUTHPRIV" - if not device_params['cliTransport']: - device_params['cliTransport'] = "ssh" + if not device_params['cliTransport']: + device_params['cliTransport'] = "ssh" - if not device_params['snmpPrivProtocol']: - device_params['snmpPrivProtocol'] = "AES128" + if not device_params['snmpPrivProtocol']: + device_params['snmpPrivProtocol'] = "AES128" - if device_params['snmpPrivProtocol'] == "AES192": - device_params['snmpPrivProtocol'] = "CISCOAES192" - elif device_params['snmpPrivProtocol'] == "AES256": - device_params['snmpPrivProtocol'] = "CISCOAES256" + if device_params['snmpPrivProtocol'] == "AES192": + device_params['snmpPrivProtocol'] = "CISCOAES192" + elif device_params['snmpPrivProtocol'] == "AES256": + device_params['snmpPrivProtocol'] = "CISCOAES256" - 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) + 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: @@ -2970,6 +3181,14 @@ def get_diff_merged(self, config): netconf port as None to perform the device update task""".format(playbook_params['netconfPort']), "DEBUG") playbook_params['netconfPort'] = None + if not playbook_params['snmpVersion']: + playbook_params['snmpVersion'] = device_data['snmp_version'] + + if playbook_params['snmpVersion'] == '2c': + params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] + for param in params_to_remove: + playbook_params.pop(param, None) + try: if playbook_params['updateMgmtIPaddressList']: new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] @@ -3130,10 +3349,13 @@ def get_diff_merged(self, config): if self.config[0].get('provision_wired_device'): self.provisioned_wired_device().check_return_status() + # This will be used to re-provisioned the wired device in inventory + if self.config[0].get('reprovision_wired_device'): + self.reprovisioned_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() + self.provisioned_wireless_devices().check_return_status() if device_resynced: self.resync_devices().check_return_status() @@ -3415,6 +3637,26 @@ def verify_diff_merged(self, config): self.log("""Mismatch between playbook's input and Cisco Catalyst Center detected, indicating that the provisioning task may not have executed successfully.""", "INFO") + if self.config[0].get('reprovision_wired_device'): + reprovision_wired_list = self.config[0].get('reprovision_wired_device') + re_provision_flag = True + reprovision_device_list = [] + + for prov_dict in reprovision_wired_list: + device_ip = prov_dict['device_ip'] + reprovision_device_list.append(device_ip) + if not self.get_provision_wired_device(device_ip): + re_provision_flag = False + break + + if re_provision_flag: + self.status = "success" + msg = "Wired devices {0} get re-provisioned and verified successfully.".format(reprovision_device_list) + self.log(msg, "INFO") + else: + self.log("""Mismatch between playbook's input and Cisco Catalyst Center detected, indicating that + the re-provisioning task may not have executed successfully.""", "INFO") + return self def verify_diff_deleted(self, config): diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 5c2f60afda..c6290f0cdc 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -92,6 +92,9 @@ 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. + NETCONF with user privilege 15 is mandatory for enabling Wireless Services on Wireless capable devices such as Catalyst 9000 series + Switches and C9800 Series Wireless Controllers. The NETCONF credentials are required to connect to C9800 Series Wireless Controllers + as the majority of data collection is done using NETCONF for these Devices. type: str username: description: Username for accessing the device. Required for Adding Network Device. @@ -141,9 +144,14 @@ description: SNMP username required for adding network, compute, and third-party devices. type: str snmp_version: - description: Device's snmp Version. + description: It is a standard protocol used for managing and monitoring network devices. + v2 - In this communication between the SNMP manager (such as Cisco Catalyst) and the managed devices + (such as routers, switches, or access points) is based on community strings.Community strings serve + as form of authentication and they are transmitted in clear text, providing no encryption. + v3 - It is the most secure version of SNMP, providing authentication, integrity, and encryption features. + It allows for the use of usernames, authentication passwords, and encryption keys, providing stronger + security compared to v2. type: str - 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 @@ -253,10 +261,6 @@ description: List of device parameters that needs to be exported to file. type: list elements: str - managed_ap_locations: - description: Location of the sites allocated for the APs - type: list - elements: str provision_wired_device: description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wired device and the name of the site where the device will be provisioned. @@ -281,30 +285,73 @@ process. If unspecified, the system will check the device status every 2 seconds by default. type: int default: 2 - dynamic_interfaces: - description: Interface details of the wireless device + reprovision_wired_device: + description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wired device and + the name of the site where the device will be re-provisioned. type: list elements: dict suboptions: - interface_ip_address: - description: Ip Address allocated to the interface + device_ip: + description: Specifies the IP address of the wired device. This is a string value that should be in the format of + standard IPv4 or IPv6 addresses. 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. + site_name: + description: Indicates the exact location where the wired device will be provisioned. This is a string value that should + represent the complete hierarchical path of the site (For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). type: str - lag_or_port_number: - description: The Link Aggregation Group (LAG) number or port number assigned to the interface. + provision_wireless_device: + description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wireless device and + the name of the site where the device will be provisioned along with dynamic interface details. + type: list + elements: dict + suboptions: + device_ip: + description: Specifies the IP address of the wirelesss device. This is a string value that should be in the format of + standard IPv4 or IPv6 addresses. + type: str + site_name: + description: Indicates the exact location where the wired device will be provisioned. This is a string value that should + represent the complete hierarchical path of the site (For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). + type: str + managed_ap_locations: + description: Location of the sites allocated for the APs (For example, ["Global/USA/San Francisco/BGL_18/floor_test", + "Global/USA/San Francisco/BGL_18/floor_check"]) + 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 + resync_retry_count: + description: Determines the total number of retry attempts for checking if the device has reached a managed state during + the provisioning process. If unspecified, the default value is set to 200 retries. type: int - vlan_id: - description: The VLAN (Virtual Local Area Network) ID associated with the network interface. + default: 200 + resync_retry_interval: + description: Sets the interval, in seconds, at which the system will recheck the device status throughout the provisioning + process. If unspecified, the system will check the device status every 2 seconds by default. type: int - interface_name: - description: Name of the interface. - type: str + default: 2 requirements: - dnacentersdk >= 2.5.5 @@ -523,6 +570,25 @@ resync_retry_count: 200 resync_retry_interval: 2 +- name: Re-Provisioned Wired Devices to site in Inventory + cisco.dnac.inventory_intent: + 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: + - reprovision_wired_device: + - device_ip: "1.1.1.1" + site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" + - device_ip: "2.2.2.2" + site_name: "Global/USA/San Francisco/BGL_18/floor_test" + - name: Associate Wireless Devices to site and Provisioned it in Inventory cisco.dnac.inventory_workflow_manager: dnac_host: "{{dnac_host}}" @@ -536,17 +602,31 @@ dnac_log: False state: merged config: - - ip_address_list: ["1.1.1.1", "2.2.2.2"] provision_wireless_device: - 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: 23.23.21.12 - interface_netmask_in_cidr: 24 - interface_gateway: "gateway" - lag_or_port_number: 12 - vlan_id: 99 - interface_name: "etherenet0/0" + - device_ip: "1.1.1.1" + site_name: "Global/USA/BGL_18/floor_pnp" + managed_ap_locations: ["Global/USA/BGL_18/floor_pnp", "Global/USA/BGL_18/floor_test"] + dynamic_interfaces: + - 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" + resync_retry_count: 200 + resync_retry_interval: 2 + - device_ip: "2.2.2.2" + site_name: "Global/USA/BGL_18/floor_test" + managed_ap_locations: ["Global/USA/BGL_19/floor_pnp", "Global/USA/BGL_19/floor_test"] + dynamic_interfaces: + - interface_ip_address: 32.31.12.23 + interface_netmask_in_cidr: 26 + interface_gateway: "gateway_test" + lag_or_port_number: 33 + vlan_id: 78 + interface_name: "etherenet1/1" + resync_retry_count: 200 + resync_retry_interval: 2 - name: Update Device Role with IP Address cisco.dnac.inventory_workflow_manager: @@ -784,7 +864,7 @@ def validate_input(self): 'snmp_retry': {'default': 3, 'type': 'int'}, 'snmp_timeout': {'default': 5, 'type': 'int'}, 'snmp_username': {'type': 'str'}, - 'snmp_version': {'default': "v3", 'type': 'str'}, + 'snmp_version': {'type': 'str'}, 'update_mgmt_ipaddresslist': {'type': 'list', 'elements': 'dict'}, 'username': {'type': 'str'}, 'update_device_role': {'type': 'dict'}, @@ -822,8 +902,14 @@ def validate_input(self): 'resync_retry_count': {'default': 200, 'type': 'int'}, 'resync_retry_interval': {'default': 2, 'type': 'int'}, }, + 'reprovision_wired_device': { + 'type': 'list', + 'device_ip': {'type': 'str'}, + 'site_name': {'type': 'str'}, + }, 'provision_wireless_device': { 'type': 'list', + 'device_ip': {'type': 'str'}, 'site_name': {'type': 'str'}, 'managed_ap_locations': {'type': 'list', 'elements': 'str'}, 'dynamic_interfaces': { @@ -835,6 +921,8 @@ def validate_input(self): 'vlan_id': {'type': 'int'}, 'interface_name': {'type': 'str'}, }, + 'resync_retry_count': {'default': 200, 'type': 'int'}, + 'resync_retry_interval': {'default': 2, 'type': 'int'}, } } @@ -1603,7 +1691,7 @@ def provisioned_wired_device(self): if not site_name or not device_ip: self.status = "failed" - self.msg = "Site/Devices are required for Provisioning of Wired Devices." + self.msg = "Site and Device IP are required for Provisioning of Wired Devices." self.log(self.msg, "ERROR") self.result['response'] = self.msg return self @@ -1663,7 +1751,6 @@ def provisioned_wired_device(self): 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) @@ -1693,12 +1780,102 @@ def provisioned_wired_device(self): return self - def get_wireless_param(self, device_ip): + def reprovisioned_wired_device(self): + """ + Re-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 re-provision wired devices in Cisco Catalyst Center based on the configuration provided. + It retrieves the site name and IP addresses of the devices from the list of configuration, + attempts to provision each device with site, and monitors the provisioning process. + """ + + reprovision_wired_list = self.config[0]['reprovision_wired_device'] + total_devices_to_reprovisioned = len(reprovision_wired_list) + device_in_ccc = self.device_exists_in_ccc() + device_ip_list = [] + provision_count, already_provision_count = 0, 0 + + for prov_dict in reprovision_wired_list: + device_ip = prov_dict['device_ip'] + device_ip_list.append(device_ip) + site_name = prov_dict['site_name'] + device_type = "Wired" + + if device_ip not in device_in_ccc: + self.msg = "Device '{0}' not present in Cisco Catalyst Center so cannot re-provisioned it.".format(device_ip) + self.log(self.msg, "WARNING") + continue + + if not site_name or not device_ip: + self.status = "failed" + self.msg = "Site/Devices are required for Re-Provisioning of Wired Devices." + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + reprovision_wired_params = { + 'deviceManagementIpAddress': device_ip, + 'siteNameHierarchy': site_name + } + + try: + response = self.dnac._exec( + family="sda", + function='re_provision_wired_device', + op_modifies=True, + params=reprovision_wired_params, + ) + + if response.get("status") == "failed": + description = response.get("description") + error_msg = "Cannot do Re-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("data") + + if 'processcfs_complete=true' 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 == total_devices_to_reprovisioned: + self.handle_all_already_provisioned(device_ip_list, device_type) + elif provision_count == total_devices_to_reprovisioned: + 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, prov_dict): """ 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. + prov_dict (dict): A dictionary containing configuration parameters for wireless provisioning. Returns: wireless_param (list of dict): A list containing a dictionary with wireless provisioning parameters. Description: @@ -1710,50 +1887,52 @@ def get_wireless_param(self, device_ip): 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'], - } - ] + try: + device_ip_address = prov_dict['device_ip'] + site_name = prov_dict['site_name'] - 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 = [ + { + 'site': site_name, + 'managedAPLocations': prov_dict['managed_ap_locations'], + } + ] - wireless_param[0]["dynamicInterfaces"] = [] + 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 prov_dict.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) - 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_address} + ) - 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 the parameters required for Wireless Provisioning", "DEBUG") - 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") + except Exception as e: + self.msg = """An exception occured while fetching the details for wireless provisioning of + device '{0}' due to - {1}""".format(device_ip_address, str(e)) + self.log(self.msg, "ERROR") return self @@ -1799,12 +1978,11 @@ def get_site_type(self, site_name): return site_type - def provisioned_wireless_devices(self, device_ips): + def provisioned_wireless_devices(self): """ 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: @@ -1816,25 +1994,23 @@ def provisioned_wireless_devices(self, device_ips): provision_count, already_provision_count = 0, 0 device_type = "Wireless" + device_ip_list = [] + provision_wireless_list = self.config[0]['provision_wireless_device'] - 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: + for prov_dict in provision_wireless_list: try: # Collect the device parameters from the playbook to perform wireless provisioing - self.get_wireless_param(device_ip).check_return_status() + self.get_wireless_param(prov_dict).check_return_status() + device_ip = prov_dict['device_ip'] + device_ip_list.append(device_ip) provisioning_params = self.wireless_param - count = 1 + resync_retry_count = prov_dict.get("resync_retry_count", 200) + # This resync retry interval will be in seconds which will check device status at given interval + resync_retry_interval = prov_dict.get("resync_retry_interval", 2) managed_flag = True # Check till device comes into managed state - while True: + while resync_retry_count: response = self.get_device_response(device_ip) self.log("Device is in {0} state waiting for Managed State.".format(response['managementState']), "DEBUG") @@ -1843,16 +2019,26 @@ def provisioned_wireless_devices(self, device_ips): and response.get('collectionStatus') == "Managed" and response.get("hostname") ): + msg = """Device '{0}' comes to managed state and ready for provisioning with the resync_retry_count + '{1}' left having resync interval of {2} seconds""".format(device_ip, resync_retry_count, resync_retry_interval) + self.log(msg, "INFO") + managed_flag = True break - count = count + 1 - if count > 200: + if response.get('collectionStatus') == "Partial Collection Failure" or response.get('collectionStatus') == "Could Not Synchronize": + device_status = response.get('collectionStatus') + msg = """Device '{0}' comes to '{1}' state and never goes for provisioning with the resync_retry_count + '{2}' left having resync interval of {3} seconds""".format(device_ip, device_status, resync_retry_count, resync_retry_interval) + self.log(msg, "INFO") managed_flag = False break + time.sleep(resync_retry_interval) + resync_retry_count = resync_retry_count - 1 + 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') + 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 @@ -1874,7 +2060,7 @@ def provisioned_wireless_devices(self, device_ips): 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 @@ -1893,9 +2079,9 @@ def provisioned_wireless_devices(self, device_ips): 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): + if already_provision_count == len(device_ip_list): + self.handle_all_already_provisioned(device_ip_list, device_type) + elif provision_count == len(device_ip_list): self.handle_all_provisioned(device_type) elif provision_count == 0: self.handle_all_failed_provision(device_type) @@ -1949,7 +2135,7 @@ def mandatory_parameter(self): device_type = self.config[0].get("type", "NETWORK_DEVICE") params_dict = { - "NETWORK_DEVICE": ["enable_password", "ip_address_list", "password", "snmp_username", "username"], + "NETWORK_DEVICE": ["enable_password", "ip_address_list", "password", "username"], "COMPUTE_DEVICE": ["ip_address_list", "http_username", "http_password", "http_port", "snmp_username"], "MERAKI_DASHBOARD": ["http_password"], "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address_list", "http_username", "http_password"], @@ -2013,6 +2199,16 @@ def get_have(self, config): if device_ip_address not in device_in_ccc: device_not_in_ccc.append(device_ip_address) + if self.config[0].get('provision_wireless_device'): + provision_wireless_list = self.config[0].get('provision_wireless_device') + + for prov_dict in provision_wireless_list: + device_ip_address = prov_dict['device_ip'] + if device_ip_address not in want_device and device_ip_address not in devices_in_playbook: + devices_in_playbook.append(device_ip_address) + if device_ip_address not in device_in_ccc and device_ip_address not in device_not_in_ccc: + device_not_in_ccc.append(device_ip_address) + 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 @@ -2786,31 +2982,40 @@ def get_diff_merged(self, config): # To add the devices in inventory if config['ip_address_list']: - device_params = self.want.get("device_params") + input_params = self.want.get("device_params") + device_params = input_params.copy() + + if not device_params['snmpVersion']: + device_params['snmpVersion'] = "v3" device_params['ipAddress'] = config['ip_address_list'] - if not device_params['snmpMode']: - device_params['snmpMode'] = "AUTHPRIV" + if device_params['snmpVersion'] == "v2": + params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] + for param in params_to_remove: + device_params.pop(param, None) + else: + if not device_params['snmpMode']: + device_params['snmpMode'] = "AUTHPRIV" - if not device_params['cliTransport']: - device_params['cliTransport'] = "ssh" + if not device_params['cliTransport']: + device_params['cliTransport'] = "ssh" - if not device_params['snmpPrivProtocol']: - device_params['snmpPrivProtocol'] = "AES128" + if not device_params['snmpPrivProtocol']: + device_params['snmpPrivProtocol'] = "AES128" - if device_params['snmpPrivProtocol'] == "AES192": - device_params['snmpPrivProtocol'] = "CISCOAES192" - elif device_params['snmpPrivProtocol'] == "AES256": - device_params['snmpPrivProtocol'] = "CISCOAES256" + if device_params['snmpPrivProtocol'] == "AES192": + device_params['snmpPrivProtocol'] = "CISCOAES192" + elif device_params['snmpPrivProtocol'] == "AES256": + device_params['snmpPrivProtocol'] = "CISCOAES256" - 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) + 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: @@ -2971,6 +3176,14 @@ def get_diff_merged(self, config): netconf port as None to perform the device update task""".format(playbook_params['netconfPort']), "DEBUG") playbook_params['netconfPort'] = None + if not playbook_params['snmpVersion']: + playbook_params['snmpVersion'] = device_data['snmp_version'] + + if playbook_params['snmpVersion'] == '2c': + params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] + for param in params_to_remove: + playbook_params.pop(param, None) + try: if playbook_params['updateMgmtIPaddressList']: new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] @@ -3132,10 +3345,13 @@ def get_diff_merged(self, config): if self.config[0].get('provision_wired_device'): self.provisioned_wired_device().check_return_status() + # This will be used to re-provisioned the wired device in inventory + if self.config[0].get('reprovision_wired_device'): + self.reprovisioned_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() + self.provisioned_wireless_devices().check_return_status() if device_resynced: self.resync_devices().check_return_status() @@ -3416,6 +3632,26 @@ def verify_diff_merged(self, config): self.log("""Mismatch between playbook's input and Cisco Catalyst Center detected, indicating that the provisioning task may not have executed successfully.""", "INFO") + if self.config[0].get('reprovision_wired_device'): + reprovision_wired_list = self.config[0].get('reprovision_wired_device') + re_provision_flag = True + reprovision_device_list = [] + + for prov_dict in reprovision_wired_list: + device_ip = prov_dict['device_ip'] + reprovision_device_list.append(device_ip) + if not self.get_provision_wired_device(device_ip): + re_provision_flag = False + break + + if re_provision_flag: + self.status = "success" + msg = "Wired devices {0} get re-provisioned and verified successfully.".format(reprovision_device_list) + self.log(msg, "INFO") + else: + self.log("""Mismatch between playbook's input and Cisco Catalyst Center detected, indicating that + the re-provisioning task may not have executed successfully.""", "INFO") + return self def verify_diff_deleted(self, config): From 7806bf9c7192f5610bb03e206dec25e16036bb8f Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 28 Feb 2024 14:25:29 +0530 Subject: [PATCH 11/44] Handle the edge case of adding/updating device in inventory with snmp_version as v2, Fix the bug of device role in which playbook goes to infinite loop because of wrong API response, Remove enable_password from the mandatory field while adding device , make message more clearer while deleting the list of devices. --- plugins/modules/inventory_intent.py | 34 +++++++++++-------- plugins/modules/inventory_workflow_manager.py | 33 +++++++++++------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 971b84ec92..8cd8514491 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -127,11 +127,9 @@ snmp_ro_community: description: SNMP Read-Only community required for adding V2C devices. type: str - default: public snmp_rw_community: description: SNMP Read-Write community required for adding V2C devices. type: str - default: private snmp_retry: description: SNMP retry count. type: int @@ -685,7 +683,7 @@ - ip_address_list: ["1.1.1.1", "2.2.2.2"] export_device_list: password: "File_password" - operation_enum: 0 + operation_enum: "0" parameters: ["componentName", "SerialNumber", "Last Sync Status"] - name: Create Global User Defined with IP Address @@ -859,8 +857,8 @@ def validate_input(self): 'snmp_mode': {'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_ro_community': {'type': 'str'}, + 'snmp_rw_community': {'type': 'str'}, 'snmp_retry': {'default': 3, 'type': 'int'}, 'snmp_timeout': {'default': 5, 'type': 'int'}, 'snmp_username': {'type': 'str'}, @@ -2142,11 +2140,11 @@ def mandatory_parameter(self): device_type = self.config[0].get("type", "NETWORK_DEVICE") params_dict = { - "NETWORK_DEVICE": ["enable_password", "ip_address_list", "password", "username"], - "COMPUTE_DEVICE": ["ip_address_list", "http_username", "http_password", "http_port", "snmp_username"], + "NETWORK_DEVICE": ["ip_address_list", "password", "username"], + "COMPUTE_DEVICE": ["ip_address_list", "http_username", "http_password", "http_port"], "MERAKI_DASHBOARD": ["http_password"], "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address_list", "http_username", "http_password"], - "THIRD_PARTY_DEVICE": ["ip_address_list", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"] + "THIRD_PARTY_DEVICE": ["ip_address_list"] } params_list = params_dict.get(device_type, []) @@ -3176,15 +3174,21 @@ def get_diff_merged(self, config): if playbook_params['netconfPort'] == " ": playbook_params['netconfPort'] = None + if playbook_params['enablePassword'] == " ": + playbook_params['enablePassword'] = 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 if not playbook_params['snmpVersion']: - playbook_params['snmpVersion'] = device_data['snmp_version'] + if device_data['snmp_version'] == 3: + playbook_params['snmpVersion'] = "v3" + else: + playbook_params['snmpVersion'] = "v2" - if playbook_params['snmpVersion'] == '2c': + if playbook_params['snmpVersion'] == 'v2': params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] for param in params_to_remove: playbook_params.pop(param, None) @@ -3281,8 +3285,9 @@ def get_diff_merged(self, config): while True: execution_details = self.get_task_details(task_id) + progress = execution_details.get("progress") - if 'successfully' in execution_details.get("progress"): + if 'successfully' in progress or 'succesfully' in progress: self.status = "success" self.result['changed'] = True self.result['response'] = execution_details @@ -3442,7 +3447,7 @@ def get_diff_deleted(self, config): 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.result['msg'].append(self.msg) self.result['response'] = self.msg self.log(self.msg, "INFO") continue @@ -3471,11 +3476,12 @@ def get_diff_deleted(self, config): self.result['changed'] = True self.msg = execution_details.get("bapiName") self.log(self.msg, "INFO") - self.result['response'] = self.msg + self.result['response'].append(self.msg) break elif execution_details.get("bapiError"): self.msg = execution_details.get("bapiError") self.log(self.msg, "ERROR") + self.result['response'].append(self.msg) break except Exception as e: device_id = self.get_device_ids([device_ip]) @@ -3511,7 +3517,7 @@ def get_diff_deleted(self, config): self.msg = "Device '{0}' deletion get failed.".format(device_ip) self.log(self.msg, "ERROR") break - self.result['msg'] = self.msg + self.result['msg'].append(self.msg) return self diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index c6290f0cdc..7455d790b1 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -127,11 +127,9 @@ snmp_ro_community: description: SNMP Read-Only community required for adding V2C devices. type: str - default: public snmp_rw_community: description: SNMP Read-Write community required for adding V2C devices. type: str - default: private snmp_retry: description: SNMP retry count. type: int @@ -686,7 +684,7 @@ - ip_address_list: ["1.1.1.1", "2.2.2.2"] export_device_list: password: "File_password" - operation_enum: 0 + operation_enum: "0" parameters: ["componentName", "SerialNumber", "Last Sync Status"] - name: Create Global User Defined with IP Address @@ -859,8 +857,8 @@ def validate_input(self): 'snmp_mode': {'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_ro_community': {'type': 'str'}, + 'snmp_rw_community': {'type': 'str'}, 'snmp_retry': {'default': 3, 'type': 'int'}, 'snmp_timeout': {'default': 5, 'type': 'int'}, 'snmp_username': {'type': 'str'}, @@ -2135,11 +2133,11 @@ def mandatory_parameter(self): device_type = self.config[0].get("type", "NETWORK_DEVICE") params_dict = { - "NETWORK_DEVICE": ["enable_password", "ip_address_list", "password", "username"], - "COMPUTE_DEVICE": ["ip_address_list", "http_username", "http_password", "http_port", "snmp_username"], + "NETWORK_DEVICE": ["ip_address_list", "password", "username"], + "COMPUTE_DEVICE": ["ip_address_list", "http_username", "http_password", "http_port"], "MERAKI_DASHBOARD": ["http_password"], "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address_list", "http_username", "http_password"], - "THIRD_PARTY_DEVICE": ["ip_address_list", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"] + "THIRD_PARTY_DEVICE": ["ip_address_list"] } params_list = params_dict.get(device_type, []) @@ -3171,15 +3169,21 @@ def get_diff_merged(self, config): if playbook_params['netconfPort'] == " ": playbook_params['netconfPort'] = None + if playbook_params['enablePassword'] == " ": + playbook_params['enablePassword'] = 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 if not playbook_params['snmpVersion']: - playbook_params['snmpVersion'] = device_data['snmp_version'] + if device_data['snmp_version'] == 3: + playbook_params['snmpVersion'] = "v3" + else: + playbook_params['snmpVersion'] = "v2" - if playbook_params['snmpVersion'] == '2c': + if playbook_params['snmpVersion'] == 'v2': params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] for param in params_to_remove: playbook_params.pop(param, None) @@ -3276,8 +3280,9 @@ def get_diff_merged(self, config): while True: execution_details = self.get_task_details(task_id) + progress = execution_details.get("progress") - if 'successfully' in execution_details.get("progress"): + if 'successfully' in progress or 'succesfully' in progress: self.status = "success" self.result['changed'] = True self.result['response'] = execution_details @@ -3437,7 +3442,7 @@ def get_diff_deleted(self, config): 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.result['msg'].append(self.msg) self.result['response'] = self.msg self.log(self.msg, "INFO") continue @@ -3467,9 +3472,11 @@ def get_diff_deleted(self, config): self.msg = execution_details.get("bapiName") self.log(self.msg, "INFO") self.result['response'] = self.msg + self.result['msg'].append(self.msg) break elif execution_details.get("bapiError"): self.msg = execution_details.get("bapiError") + self.result['msg'].append(self.msg) self.log(self.msg, "ERROR") break except Exception as e: @@ -3506,7 +3513,7 @@ def get_diff_deleted(self, config): self.msg = "Device '{0}' deletion get failed.".format(device_ip) self.log(self.msg, "ERROR") break - self.result['msg'] = self.msg + self.result['msg'].append(self.msg) return self From 475db8c252cd18a142c8c991df3c5bd178d3b9bd Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 28 Feb 2024 16:05:47 +0530 Subject: [PATCH 12/44] Handle edge case while adding device with snmpVersion v2 --- plugins/modules/inventory_intent.py | 6 ++++++ plugins/modules/inventory_workflow_manager.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 8cd8514491..696512a29b 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2998,6 +2998,12 @@ def get_diff_merged(self, config): params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] for param in params_to_remove: device_params.pop(param, None) + + if not device_params['snmpROCommunity']: + self.status = "failed" + self.msg = "Required parameter 'snmpROCommunity' for adding device with snmmp version v2 is not present" + self.result['msg'] = self.msg + self.log(self.msg, "ERROR") else: if not device_params['snmpMode']: device_params['snmpMode'] = "AUTHPRIV" diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 7455d790b1..63990315cf 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -2991,6 +2991,12 @@ def get_diff_merged(self, config): params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] for param in params_to_remove: device_params.pop(param, None) + + if not device_params['snmpROCommunity']: + self.status = "failed" + self.msg = "Required parameter 'snmpROCommunity' for adding device with snmmp version v2 is not present" + self.result['msg'] = self.msg + self.log(self.msg, "ERROR") else: if not device_params['snmpMode']: device_params['snmpMode'] = "AUTHPRIV" From 5ac7717b5c16d375276d4d62ad070193db5d6c34 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 28 Feb 2024 18:04:14 +0530 Subject: [PATCH 13/44] return failed if device required parameter missing while adding device --- plugins/modules/inventory_intent.py | 8 ++++---- plugins/modules/inventory_workflow_manager.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 696512a29b..c5c56df17b 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2984,9 +2984,8 @@ def get_diff_merged(self, config): self.log(self.msg, "INFO") self.result['changed'] = False self.result['response'] = self.msg - - # To add the devices in inventory - if config['ip_address_list']: + else: + # To add the devices in inventory input_params = self.want.get("device_params") device_params = input_params.copy() @@ -3004,6 +3003,7 @@ def get_diff_merged(self, config): self.msg = "Required parameter 'snmpROCommunity' for adding device with snmmp version v2 is not present" self.result['msg'] = self.msg self.log(self.msg, "ERROR") + return self else: if not device_params['snmpMode']: device_params['snmpMode'] = "AUTHPRIV" @@ -3067,7 +3067,7 @@ def get_diff_merged(self, config): self.msg = "Device addition get failed" self.log(self.msg, "ERROR") self.result['msg'] = self.msg - break + return self except Exception as e: error_message = "Error while adding device in Cisco Catalyst Center: {0}".format(str(e)) diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 63990315cf..cdda1ef847 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -2977,9 +2977,8 @@ def get_diff_merged(self, config): self.log(self.msg, "INFO") self.result['changed'] = False self.result['response'] = self.msg - - # To add the devices in inventory - if config['ip_address_list']: + else: + # To add the devices in inventory input_params = self.want.get("device_params") device_params = input_params.copy() @@ -2997,6 +2996,7 @@ def get_diff_merged(self, config): self.msg = "Required parameter 'snmpROCommunity' for adding device with snmmp version v2 is not present" self.result['msg'] = self.msg self.log(self.msg, "ERROR") + return self else: if not device_params['snmpMode']: device_params['snmpMode'] = "AUTHPRIV" From ccf4036402b3121172e00798c0da276cfd7d1d06 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 29 Feb 2024 09:57:48 +0530 Subject: [PATCH 14/44] add string while checking snmp_version for updating device details --- plugins/modules/inventory_intent.py | 2 +- plugins/modules/inventory_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index c5c56df17b..8cbb194357 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -3189,7 +3189,7 @@ def get_diff_merged(self, config): playbook_params['netconfPort'] = None if not playbook_params['snmpVersion']: - if device_data['snmp_version'] == 3: + if device_data['snmp_version'] == '3': playbook_params['snmpVersion'] = "v3" else: playbook_params['snmpVersion'] = "v2" diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index cdda1ef847..01f12dc9b4 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -3184,7 +3184,7 @@ def get_diff_merged(self, config): playbook_params['netconfPort'] = None if not playbook_params['snmpVersion']: - if device_data['snmp_version'] == 3: + if device_data['snmp_version'] == '3': playbook_params['snmpVersion'] = "v3" else: playbook_params['snmpVersion'] = "v2" From 661cc17c4f7e5dfbc7598e54cb515275464ed139 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 29 Feb 2024 10:55:47 +0530 Subject: [PATCH 15/44] Fix the issue of un-tagging golden image in a child site should return failed with msg that cannot be un-tagged --- plugins/modules/swim_intent.py | 8 +++++--- plugins/modules/swim_workflow_manager.py | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 308205ea5d..1447a9a609 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -58,6 +58,7 @@ suboptions: file_path: description: Provide the absolute file path needed to import an image from your local system (Eg "/path/to/your/file"). + Accepted files formats are - .gz,.bin,.img,.tar,.smu,.pie,.aes,.iso,.ova,.tar_gz,.qcow2,.nfvispkg,.zip,.spa,.rpm. type: str is_third_party: description: Query parameter to determine if the image is from a third party (optional). @@ -130,7 +131,8 @@ 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 - to import an image. + to import an image.(For example, http://{host}/swim/cat9k_isoxe.16.12.10s.SPA.bin, + ftp://user:password@{host}/swim/cat9k_isoxe.16.12.10s.SPA.iso) type: str is_third_party: description: Flag indicates whether the image is uploaded from a third party (optional). @@ -1183,11 +1185,11 @@ def get_diff_tagging(self): 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.status = "failed" self.result['changed'] = False self.msg = failure_reason self.result['msg'] = failure_reason - self.log(self.msg, "WARNING") + self.log(self.msg, "ERROR") else: error_message = task_details.get("failureReason", "Error: while tagging/un-tagging the golden swim image.") self.status = "failed" diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 7148a514ac..3594bdd315 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -58,6 +58,7 @@ suboptions: file_path: description: Provide the absolute file path needed to import an image from your local system (Eg "/path/to/your/file"). + Accepted files formats are - .gz,.bin,.img,.tar,.smu,.pie,.aes,.iso,.ova,.tar_gz,.qcow2,.nfvispkg,.zip,.spa,.rpm. type: str is_third_party: description: Query parameter to determine if the image is from a third party (optional). @@ -130,7 +131,8 @@ 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 - to import an image. + to import an image..(For example, http://{host}/swim/cat9k_isoxe.16.12.10s.SPA.bin, + ftp://user:password@{host}/swim/cat9k_isoxe.16.12.10s.SPA.iso) type: str is_third_party: description: Flag indicates whether the image is uploaded from a third party (optional). @@ -1169,11 +1171,11 @@ def get_diff_tagging(self): 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.status = "failed" self.result['changed'] = False self.msg = failure_reason self.result['msg'] = failure_reason - self.log(self.msg, "WARNING") + self.log(self.msg, "ERROR") else: error_message = task_details.get("failureReason", "Error: while tagging/un-tagging the golden swim image.") self.status = "failed" From 16bb5bb396fe09de03746839b705d14a831ab917 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 29 Feb 2024 12:22:49 +0530 Subject: [PATCH 16/44] Address the issue of updating snmp_auth_passphrase and snmp_priv_passphrase --- plugins/modules/inventory_intent.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 8cbb194357..f0eea96d51 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -3160,8 +3160,10 @@ def get_diff_merged(self, config): 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 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['snmpPrivProtocol'] == "AES192": playbook_params['snmpPrivProtocol'] = "CISCOAES192" From 994f68019cffba6f7bc265a152496a01a5c38dc8 Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 29 Feb 2024 19:15:52 +0000 Subject: [PATCH 17/44] Adding option to specify global credentials while performing discovery --- playbooks/discovery_intent.yml | 72 +++- playbooks/discovery_workflow_manager.yml | 72 +++- plugins/modules/discovery_intent.py | 372 +++++++++++++++--- plugins/modules/discovery_workflow_manager.py | 372 +++++++++++++++--- 4 files changed, 778 insertions(+), 110 deletions(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index 037b247504..10e79aa71d 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -20,6 +20,76 @@ dnac_log_level: DEBUG tasks: + + - name: Execute discovery devices using MULTI RANGE with various global credentials + cisco.dnac.discovery_intent: + <<: *dnac_login + state: merged + config_verify: True + config: + - ip_address_list: + - 204.1.2.1-204.1.2.5 + - 204.192.3.40 + - 204.192.4.200 + - 204.1.2.6 + - 204.1.2.7 + - 204.1.2.8 + - 204.1.2.9 + - 204.1.2.10 + - 204.1.2.11 + discovery_type: MULTI RANGE + protocol_order: ssh + state: merged + discovery_name: Multi_global + global_credentials: + cli_credentials_list: + - description: ISE + username: cisco + - description: CLI1234 #Incorrect name passed + username: cli + http_read_credential_list: + - description: HTTP Read + username: HTTP_Read + snmp_v3_credential_list: + - description: snmpV3 + username: snmpV3 + + - name: Execute discovery of single device using various discovery specific credentials + cisco.dnac.discovery_intent: + <<: *dnac_login + state: merged + config_verify: True + config: + - discovery_name: Single IP Discovery + discovery_type: "SINGLE" + ip_address_list: + - 204.1.2.5 + protocol_order: ssh + discovery_specific_credentials: + cli_credentials_list: + - username: cisco + password: Cisco#123 + enable_password: Cisco#123 + http_read_credential: + username: string + password: Lablab#123 + port: 443 + secure: True + snmp_v2_read_credential: + desc: string + community: string + snmp_v2_write_credential: + desc: string + community: string + snmp_v3_credential: + username: v3Public2 + snmp_mode: AUTHPRIV + auth_type: SHA + auth_password: Lablab#1234 + privacy_type: AES256 + privacy_password: Lablab#1234 + global_cli_len: 3 + - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials cisco.dnac.discovery_intent: <<: *dnac_login @@ -68,7 +138,7 @@ - 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 + cdp_level: 2 #Instead use lldp for LLDP and prefix length for CIDR discovery_name: CDP_Test_1 discovery_specific_credentials: cli_credentials_list: diff --git a/playbooks/discovery_workflow_manager.yml b/playbooks/discovery_workflow_manager.yml index 3ae75fb3bc..2eea576907 100644 --- a/playbooks/discovery_workflow_manager.yml +++ b/playbooks/discovery_workflow_manager.yml @@ -20,6 +20,76 @@ dnac_log_level: DEBUG tasks: + + - name: Execute discovery devices using MULTI RANGE with various global credentials + cisco.dnac.discovery_workflow_manager: + <<: *dnac_login + state: merged + config_verify: True + config: + - ip_address_list: + - 204.1.2.1-204.1.2.5 + - 204.192.3.40 + - 204.192.4.200 + - 204.1.2.6 + - 204.1.2.7 + - 204.1.2.8 + - 204.1.2.9 + - 204.1.2.10 + - 204.1.2.11 + discovery_type: MULTI RANGE + protocol_order: ssh + state: merged + discovery_name: Multi_global + global_credentials: + cli_credentials_list: + - description: ISE + username: cisco + - description: CLI1234 #Incorrect name passed + username: cli + http_read_credential_list: + - description: HTTP Read + username: HTTP_Read + snmp_v3_credential_list: + - description: snmpV3 + username: snmpV3 + + - name: Execute discovery of single device using various discovery specific credentials + cisco.dnac.discovery_workflow_manager: + <<: *dnac_login + state: merged + config_verify: True + config: + - discovery_name: Single IP Discovery + discovery_type: "SINGLE" + ip_address_list: + - 204.1.2.5 + protocol_order: ssh + discovery_specific_credentials: + cli_credentials_list: + - username: cisco + password: Cisco#123 + enable_password: Cisco#123 + http_read_credential: + username: string + password: Lablab#123 + port: 443 + secure: True + snmp_v2_read_credential: + desc: string + community: string + snmp_v2_write_credential: + desc: string + community: string + snmp_v3_credential: + username: v3Public2 + snmp_mode: AUTHPRIV + auth_type: SHA + auth_password: Lablab#1234 + privacy_type: AES256 + privacy_password: Lablab#1234 + global_cli_len: 3 + - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials cisco.dnac.discovery_workflow_manager: <<: *dnac_login @@ -68,7 +138,7 @@ - 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 + cdp_level: 2 #Instead use lldp for LLDP and prefix length for CIDR discovery_name: CDP_Test_1 discovery_specific_credentials: cli_credentials_list: diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index b6f9ab7ecf..bc07237292 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -200,6 +200,101 @@ - Requires valid SSH credentials to work. - Avoid standard ports like 22, 80, and 8080. type: str + global_credentials: + description: + - Credentials that are already created by the user under Device Credentials in Cisco Catalyst Center. + - If user doesn't pass any global credentials in the playbook. + - By default we will use all the global credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) + type: dict + suboptions: + cli_credentials_list: + description: + - List of Global CLI credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for CLI authentication, mandatory when using global CLI credentials. + type: str + description: + description: Name of the CLI credential, mandatory when using global CLI credentials. + type: str + http_read_credential_list: + description: + - List of Global HTTP Read credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for HTTP Read authentication, mandatory when using global HTTP credentials. + type: str + description: + description: Name of the HTTP Read credential, mandatory when using global HTTP credentials. + type: str + http_write_credential_list: + description: + - List of Global HTTP Write credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for HTTP Write authentication, mandatory when using global HTTP credentials. + type: str + description: + description: Name of the HTTP Write credential, mandatory when using global HTTP credentials. + type: str + snmp_v2_read_credential_list: + description: + - List of Global SNMP V2 Read credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for SNMP Read authentication, mandatory when using global SNMP credentials. + type: str + description: + description: Name of the SNMP Read credential, mandatory when using global SNMP credentials. + type: str + snmp_v2_write_credential_list: + description: + - List of Global SNMP V2 Write credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for SNMP Write authentication, mandatory when using global SNMP credentials. + type: str + description: + description: Name of the SNMP Write credential, mandatory when using global SNMP credentials. + type: str + snmp_v3_credential_list: + description: + - List of Global SNMP V3 credentials to be used during device discovery, giving read and write mode. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for SNMP V3 authentication, mandatory when using global SNMP credentials. + type: str + description: + description: Name of the SNMP V3 credential, mandatory when using global SNMP credentials. + type: str + net_conf_port_list: + description: + - List of Global Net conf ports to be used during device discovery. + - Generally it is advised to create device credentials with unique description. + type: list + elements: dict + suboptions: + description: + description: Name of the Net Conf Port credential, mandatory when using global Net conf port. + type: str ip_filter_list: description: List of IP adddrsess that needs to get filtered out from the IP addresses passed. type: list @@ -224,10 +319,6 @@ timeout: description: Time to wait for device response in seconds type: int - global_cli_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 @@ -286,7 +377,6 @@ protocol_order: string retry: integer timeout: integer - global_cli_len: integer discovery_specific_credentials: cli_credentials_list: - username: string @@ -315,6 +405,28 @@ auth_type: string privacy_type: string privacy_password: string + net_conf_port: string + global_credentials: + cli_credentials_list: + - description: string + username: string + http_read_credential_list: + - description: string + username: string + http_write_credential_list: + - description: string + username: string + snmp_v3_credential_list: + - description: string + username: string + snmp_v2_read_credential_list: + - description: string + username: string + snmp_v2_write_credential_list: + - description: string + username: string + net_conf_port_list: + - description: string - name: Delete disovery by name cisco.dnac.discovery_intent: @@ -443,8 +555,7 @@ def validate_input(self, state=None): 'default': 'None'}, 'retry': {'type': 'int', 'required': False}, 'timeout': {'type': 'str', 'required': False}, - 'global_cli_len': {'type': 'int', 'required': False, - 'default': 1} + 'global_credentials': {'type': 'dict', 'required': False} } if state == "merged": @@ -490,6 +601,164 @@ 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 handle_global_credentials(self, response=None): + """ + Method to convert values for create_params API when global paramters + are passed as input. + + Parameters: + - response: The response collected from the get_all_global_credentials_v2 API + + Returns: + - global_cred_dict : The dictionary containing list of IDs of various types of + Global credentials. + """ + + global_credentials = self.validated_config[0].get("global_credentials") + cli_credentials_list = global_credentials.get('cli_credentials_list') + http_read_credential_list = global_credentials.get('http_read_credential_list') + http_write_credential_list = global_credentials.get('http_write_credential_list') + snmp_v2_read_credential_list = global_credentials.get('snmp_v2_read_credential_list') + snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') + snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') + net_conf_port_list = global_credentials.get('net_conf_port_list') + global_cred_dict = {} + if cli_credentials_list: + if not isinstance(cli_credentials_list, list): + msg = "Global ClI credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(cli_credentials_list) > 0: + global_cred_dict["cliCredential"] = [] + cred_len = len(cli_credentials_list) + if cred_len > 5: + cred_len = 5 + for cli_cred in cli_credentials_list: + if cli_cred.get('description') and cli_cred.get('username'): + for cli in response.get("cliCredential"): + if cli.get("description") == cli_cred.get('description') and cli.get("username") == cli_cred.get('username'): + global_cred_dict["cliCredential"].append(cli.get("id")) + global_cred_dict["cliCredential"] = global_cred_dict["cliCredential"][:cred_len] + else: + msg = "Please provide description and username of the Global CLI credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if http_read_credential_list: + if not isinstance(http_read_credential_list, list): + msg = "Global HTTP read credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(http_read_credential_list) > 0: + global_cred_dict["httpsRead"] = [] + cred_len = len(http_read_credential_list) + if cred_len > 5: + cred_len = 5 + for http_cred in http_read_credential_list: + if http_cred.get('description') and http_cred.get('username'): + for http in response.get("httpsRead"): + if http.get("description") == http.get('description') and http.get("username") == http.get('username'): + global_cred_dict["httpsRead"].append(http.get("id")) + global_cred_dict["httpsRead"] = global_cred_dict["httpsRead"][:cred_len] + else: + msg = "Please provide description and username of the Global HTTP read credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if http_write_credential_list: + if not isinstance(http_write_credential_list, list): + msg = "Global HTTP write credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(http_write_credential_list) > 0: + global_cred_dict["httpsWrite"] = [] + cred_len = len(http_write_credential_list) + if cred_len > 5: + cred_len = 5 + for http_cred in http_write_credential_list: + if http_cred.get('description') and http_cred.get('username'): + for http in response.get("httpsWrite"): + if http.get("description") == http.get('description') and http.get("username") == http.get('username'): + global_cred_dict["httpsWrite"].append(http.get("id")) + global_cred_dict["httpsWrite"] = global_cred_dict["httpsWrite"][:cred_len] + else: + msg = "Please provide description and username of the Global HTTP write credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if snmp_v2_read_credential_list: + if not isinstance(snmp_v2_read_credential_list, list): + msg = "Global SNMPV2 read credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(snmp_v2_read_credential_list) > 0: + global_cred_dict["snmpV2cRead"] = [] + cred_len = len(snmp_v2_read_credential_list) + if cred_len > 5: + cred_len = 5 + for snmp_cred in snmp_v2_read_credential_list: + if snmp_cred.get('description'): + for snmp in response.get("snmpV2cRead"): + if snmp.get("description") == snmp_cred.get('description'): + global_cred_dict["snmpV2cRead"].append(snmp.get("id")) + global_cred_dict["snmpV2cRead"] = global_cred_dict["snmpV2cRead"][:cred_len] + else: + msg = "Please provide description of the Global SNMPV2 read credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if snmp_v3_credential_list: + if not isinstance(snmp_v3_credential_list, list): + msg = "Global SNMPV3 write credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(snmp_v3_credential_list) > 0: + global_cred_dict["snmpV3"] = [] + cred_len = len(snmp_v3_credential_list) + if cred_len > 5: + cred_len = 5 + for snmp_cred in snmp_v3_credential_list: + if snmp_cred.get('description') and snmp_cred.get('username'): + for snmp in response.get("snmpV3"): + if snmp.get("description") == snmp_cred.get('description') and snmp.get("username") == snmp_cred.get('username'): + global_cred_dict["snmpV3"].append(snmp.get("id")) + global_cred_dict["snmpV3"] = global_cred_dict["snmpV3"][:cred_len] + else: + msg = "Please provide description and username of the Global SNMPV3 credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if snmp_v2_write_credential_list: + if not isinstance(snmp_v2_write_credential_list, list): + msg = "Global SNMPV2 write credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(snmp_v2_write_credential_list) > 0: + global_cred_dict["snmpV2cWrite"] = [] + cred_len = len(snmp_v2_write_credential_list) + if cred_len > 5: + cred_len = 5 + for snmp_cred in snmp_v2_write_credential_list: + if snmp_cred.get('description'): + for snmp in response.get("snmpV2cWrite"): + if snmp.get("description") == snmp_cred.get('description'): + global_cred_dict["snmpV2cWrite"].append(snmp.get("id")) + global_cred_dict["snmpV2cWrite"] = global_cred_dict["snmpV2cWrite"][:cred_len] + else: + msg = "Please provide description of the Global SNMPV2 write credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if net_conf_port_list: + if not isinstance(net_conf_port_list, list): + msg = "Global net Conf Ports be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(net_conf_port_list) > 0: + global_cred_dict["netconfCredential"] = [] + cred_len = len(net_conf_port_list) + if cred_len > 5: + cred_len = 5 + for port in net_conf_port_list: + if port.get("description"): + for netconf in response.get("netconfCredential"): + if port.get('description') == netconf.get('description'): + global_cred_dict["netconfCredential"].append(netconf.get("id")) + global_cred_dict["netconfCredential"] = global_cred_dict["netconfCredential"][:cred_len] + else: + msg = "Please provide description of the Global Netconf port to be used" + self.discovery_specific_cred_failure(msg=msg) + + self.log("Fetched Global credentials IDs are {0}".format(global_cred_dict), "INFO") + return global_cred_dict + def get_ccc_global_credentials_v2_info(self): """ Retrieve the global credentials information (version 2). @@ -511,44 +780,31 @@ 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") + global_cred_dict = {} + global_credentials = self.validated_config[0].get("global_credentials") + if global_credentials: + global_cred_dict = self.handle_global_credentials(response=response) - 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") - 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 + global_cred_set = set(global_cred_dict.keys()) + response_cred_set = set(response.keys()) + diff_keys = response_cred_set.difference(global_cred_set) - elif total_cli < 6 and cli_len_inp > total_cli: - cli_len_inp = total_cli - - cli_len = 0 - - for key in response.keys(): + for key in diff_keys: + global_cred_dict[key] = [] 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) + total_len = len(response[key]) + if total_len > 5: + total_len = 5 + for element in response.get(key): + global_cred_dict[key].append(element.get('id')) + global_cred_dict[key] = global_cred_dict[key][:total_len] + + if global_cred_dict == {}: + msg = 'Not found any global credentials to perform discovery' + self.log(msg, "WARNING") - self.result.update(dict(credential_ids=self.creds_ids_list)) + return global_cred_dict def get_devices_list_info(self): """ @@ -631,7 +887,7 @@ def preprocess_device_discovery_handle_error(self): def discovery_specific_cred_failure(self, msg=None): """ - Method for failing discovery if there is any discrepancy in the http credentials + Method for failing discovery if there is any discrepancy in the credentials passed by the user """ @@ -767,7 +1023,7 @@ def handle_discovery_specific_credentials(self, new_object_params=None): return new_object_params - def create_params(self, credential_ids=None, ip_address_list=None): + def create_params(self, ip_address_list=None): """ Create a new parameter object based on the validated configuration, credential IDs, and IP address list. @@ -783,13 +1039,11 @@ def create_params(self, credential_ids=None, ip_address_list=None): parameters. """ - if credential_ids is None: - credential_ids = [] + 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['globalCredentialIdList'] = credential_ids 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') @@ -802,11 +1056,27 @@ def create_params(self, credential_ids=None, ip_address_list=None): if self.validated_config[0].get('discovery_specific_credentials'): self.handle_discovery_specific_credentials(new_object_params=new_object_params) + global_cred_dict = self.get_ccc_global_credentials_v2_info() + + self.log(global_cred_dict, "DEBUG") + if not (new_object_params.get('snmpUserName') or new_object_params.get('snmpROCommunityDesc') or new_object_params.get('snmpRWCommunityDesc') + or global_cred_dict.get('snmpV2cRead') or global_cred_dict.get('snmpV2cWrite') or global_cred_dict.get('snmpV3')): + msg = "Please provide atleast one valid SNMP credential to perform Discovery" + self.discovery_specific_cred_failure(msg=msg) + + if not (new_object_params.get('userNameList') or global_cred_dict.get('cliCredential')): + msg = "Please provide atleast one valid CLI credential to perform Discovery" + self.discovery_specific_cred_failure(msg=msg) + + for global_cred_list in global_cred_dict.values(): + credential_ids.extend(global_cred_list) + + new_object_params['globalCredentialIdList'] = credential_ids 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): + def create_discovery(self, 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 @@ -823,14 +1093,10 @@ def create_discovery(self, credential_ids=None, ip_address_list=None): - 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), + params=self.create_params(ip_address_list=ip_address_list), op_modifies=True, ) @@ -873,7 +1139,7 @@ def get_task_status(self, task_id=None): self.module.fail_json(msg=msg) return False - if response.get('progress') != 'In Progress': + if response.get('progress') != 'In Progress' or response.get('progress') != 'Inventory service initiating discovery': result = True self.log("The Process is completed", "INFO") break @@ -1079,7 +1345,6 @@ def get_diff_merged(self): - 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() @@ -1089,7 +1354,6 @@ def get_diff_merged(self): 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() diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 5b44c4fc2e..baed92e207 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -200,6 +200,101 @@ - Requires valid SSH credentials to work. - Avoid standard ports like 22, 80, and 8080. type: str + global_credentials: + description: + - Credentials that are already created by the user under Device Credentials in Cisco Catalyst Center. + - If user doesn't pass any global credentials in the playbook. + - By default we will use all the global credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) + type: dict + suboptions: + cli_credentials_list: + description: + - List of Global CLI credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for CLI authentication, mandatory when using global CLI credentials. + type: str + description: + description: Name of the CLI credential, mandatory when using global CLI credentials. + type: str + http_read_credential_list: + description: + - List of Global HTTP Read credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for HTTP Read authentication, mandatory when using global HTTP credentials. + type: str + description: + description: Name of the HTTP Read credential, mandatory when using global HTTP credentials. + type: str + http_write_credential_list: + description: + - List of Global HTTP Write credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for HTTP Write authentication, mandatory when using global HTTP credentials. + type: str + description: + description: Name of the HTTP Write credential, mandatory when using global HTTP credentials. + type: str + snmp_v2_read_credential_list: + description: + - List of Global SNMP V2 Read credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for SNMP Read authentication, mandatory when using global SNMP credentials. + type: str + description: + description: Name of the SNMP Read credential, mandatory when using global SNMP credentials. + type: str + snmp_v2_write_credential_list: + description: + - List of Global SNMP V2 Write credentials to be used during device discovery. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for SNMP Write authentication, mandatory when using global SNMP credentials. + type: str + description: + description: Name of the SNMP Write credential, mandatory when using global SNMP credentials. + type: str + snmp_v3_credential_list: + description: + - List of Global SNMP V3 credentials to be used during device discovery, giving read and write mode. + - Generally it is advised to create device credentials with unique username or description. + type: list + elements: dict + suboptions: + username: + description: Username for SNMP V3 authentication, mandatory when using global SNMP credentials. + type: str + description: + description: Name of the SNMP V3 credential, mandatory when using global SNMP credentials. + type: str + net_conf_port_list: + description: + - List of Global Net conf ports to be used during device discovery. + - Generally it is advised to create device credentials with unique description. + type: list + elements: dict + suboptions: + description: + description: Name of the Net Conf Port credential, mandatory when using global Net conf port. + type: str ip_filter_list: description: List of IP adddrsess that needs to get filtered out from the IP addresses passed. type: list @@ -224,10 +319,6 @@ timeout: description: Time to wait for device response in seconds type: int - global_cli_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 @@ -286,7 +377,6 @@ protocol_order: string retry: integer timeout: integer - global_cli_len: integer discovery_specific_credentials: cli_credentials_list: - username: string @@ -315,6 +405,28 @@ auth_type: string privacy_type: string privacy_password: string + net_conf_port: string + global_credentials: + cli_credentials_list: + - description: string + username: string + http_read_credential_list: + - description: string + username: string + http_write_credential_list: + - description: string + username: string + snmp_v3_credential_list: + - description: string + username: string + snmp_v2_read_credential_list: + - description: string + username: string + snmp_v2_write_credential_list: + - description: string + username: string + net_conf_port_list: + - description: string - name: Delete disovery by name cisco.dnac.discovery_workflow_manager: @@ -443,8 +555,7 @@ def validate_input(self, state=None): 'default': 'None'}, 'retry': {'type': 'int', 'required': False}, 'timeout': {'type': 'str', 'required': False}, - 'global_cli_len': {'type': 'int', 'required': False, - 'default': 1} + 'global_credentials': {'type': 'dict', 'required': False} } if state == "merged": @@ -490,6 +601,164 @@ 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 handle_global_credentials(self, response=None): + """ + Method to convert values for create_params API when global paramters + are passed as input. + + Parameters: + - response: The response collected from the get_all_global_credentials_v2 API + + Returns: + - global_cred_dict : The dictionary containing list of IDs of various types of + Global credentials. + """ + + global_credentials = self.validated_config[0].get("global_credentials") + cli_credentials_list = global_credentials.get('cli_credentials_list') + http_read_credential_list = global_credentials.get('http_read_credential_list') + http_write_credential_list = global_credentials.get('http_write_credential_list') + snmp_v2_read_credential_list = global_credentials.get('snmp_v2_read_credential_list') + snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') + snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') + net_conf_port_list = global_credentials.get('net_conf_port_list') + global_cred_dict = {} + if cli_credentials_list: + if not isinstance(cli_credentials_list, list): + msg = "Global ClI credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(cli_credentials_list) > 0: + global_cred_dict["cliCredential"] = [] + cred_len = len(cli_credentials_list) + if cred_len > 5: + cred_len = 5 + for cli_cred in cli_credentials_list: + if cli_cred.get('description') and cli_cred.get('username'): + for cli in response.get("cliCredential"): + if cli.get("description") == cli_cred.get('description') and cli.get("username") == cli_cred.get('username'): + global_cred_dict["cliCredential"].append(cli.get("id")) + global_cred_dict["cliCredential"] = global_cred_dict["cliCredential"][:cred_len] + else: + msg = "Please provide description and username of the Global CLI credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if http_read_credential_list: + if not isinstance(http_read_credential_list, list): + msg = "Global HTTP read credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(http_read_credential_list) > 0: + global_cred_dict["httpsRead"] = [] + cred_len = len(http_read_credential_list) + if cred_len > 5: + cred_len = 5 + for http_cred in http_read_credential_list: + if http_cred.get('description') and http_cred.get('username'): + for http in response.get("httpsRead"): + if http.get("description") == http.get('description') and http.get("username") == http.get('username'): + global_cred_dict["httpsRead"].append(http.get("id")) + global_cred_dict["httpsRead"] = global_cred_dict["httpsRead"][:cred_len] + else: + msg = "Please provide description and username of the Global HTTP read credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if http_write_credential_list: + if not isinstance(http_write_credential_list, list): + msg = "Global HTTP write credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(http_write_credential_list) > 0: + global_cred_dict["httpsWrite"] = [] + cred_len = len(http_write_credential_list) + if cred_len > 5: + cred_len = 5 + for http_cred in http_write_credential_list: + if http_cred.get('description') and http_cred.get('username'): + for http in response.get("httpsWrite"): + if http.get("description") == http.get('description') and http.get("username") == http.get('username'): + global_cred_dict["httpsWrite"].append(http.get("id")) + global_cred_dict["httpsWrite"] = global_cred_dict["httpsWrite"][:cred_len] + else: + msg = "Please provide description and username of the Global HTTP write credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if snmp_v2_read_credential_list: + if not isinstance(snmp_v2_read_credential_list, list): + msg = "Global SNMPV2 read credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(snmp_v2_read_credential_list) > 0: + global_cred_dict["snmpV2cRead"] = [] + cred_len = len(snmp_v2_read_credential_list) + if cred_len > 5: + cred_len = 5 + for snmp_cred in snmp_v2_read_credential_list: + if snmp_cred.get('description'): + for snmp in response.get("snmpV2cRead"): + if snmp.get("description") == snmp_cred.get('description'): + global_cred_dict["snmpV2cRead"].append(snmp.get("id")) + global_cred_dict["snmpV2cRead"] = global_cred_dict["snmpV2cRead"][:cred_len] + else: + msg = "Please provide description of the Global SNMPV2 read credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if snmp_v3_credential_list: + if not isinstance(snmp_v3_credential_list, list): + msg = "Global SNMPV3 write credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(snmp_v3_credential_list) > 0: + global_cred_dict["snmpV3"] = [] + cred_len = len(snmp_v3_credential_list) + if cred_len > 5: + cred_len = 5 + for snmp_cred in snmp_v3_credential_list: + if snmp_cred.get('description') and snmp_cred.get('username'): + for snmp in response.get("snmpV3"): + if snmp.get("description") == snmp_cred.get('description') and snmp.get("username") == snmp_cred.get('username'): + global_cred_dict["snmpV3"].append(snmp.get("id")) + global_cred_dict["snmpV3"] = global_cred_dict["snmpV3"][:cred_len] + else: + msg = "Please provide description and username of the Global SNMPV3 credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if snmp_v2_write_credential_list: + if not isinstance(snmp_v2_write_credential_list, list): + msg = "Global SNMPV2 write credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(snmp_v2_write_credential_list) > 0: + global_cred_dict["snmpV2cWrite"] = [] + cred_len = len(snmp_v2_write_credential_list) + if cred_len > 5: + cred_len = 5 + for snmp_cred in snmp_v2_write_credential_list: + if snmp_cred.get('description'): + for snmp in response.get("snmpV2cWrite"): + if snmp.get("description") == snmp_cred.get('description'): + global_cred_dict["snmpV2cWrite"].append(snmp.get("id")) + global_cred_dict["snmpV2cWrite"] = global_cred_dict["snmpV2cWrite"][:cred_len] + else: + msg = "Please provide description of the Global SNMPV2 write credential to be used" + self.discovery_specific_cred_failure(msg=msg) + + if net_conf_port_list: + if not isinstance(net_conf_port_list, list): + msg = "Global net Conf Ports be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(net_conf_port_list) > 0: + global_cred_dict["netconfCredential"] = [] + cred_len = len(net_conf_port_list) + if cred_len > 5: + cred_len = 5 + for port in net_conf_port_list: + if port.get("description"): + for netconf in response.get("netconfCredential"): + if port.get('description') == netconf.get('description'): + global_cred_dict["netconfCredential"].append(netconf.get("id")) + global_cred_dict["netconfCredential"] = global_cred_dict["netconfCredential"][:cred_len] + else: + msg = "Please provide description of the Global Netconf port to be used" + self.discovery_specific_cred_failure(msg=msg) + + self.log("Fetched Global credentials IDs are {0}".format(global_cred_dict), "INFO") + return global_cred_dict + def get_ccc_global_credentials_v2_info(self): """ Retrieve the global credentials information (version 2). @@ -511,44 +780,31 @@ 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") + global_cred_dict = {} + global_credentials = self.validated_config[0].get("global_credentials") + if global_credentials: + global_cred_dict = self.handle_global_credentials(response=response) - 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") - 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 + global_cred_set = set(global_cred_dict.keys()) + response_cred_set = set(response.keys()) + diff_keys = response_cred_set.difference(global_cred_set) - elif total_cli < 6 and cli_len_inp > total_cli: - cli_len_inp = total_cli - - cli_len = 0 - - for key in response.keys(): + for key in diff_keys: + global_cred_dict[key] = [] 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) + total_len = len(response[key]) + if total_len > 5: + total_len = 5 + for element in response.get(key): + global_cred_dict[key].append(element.get('id')) + global_cred_dict[key] = global_cred_dict[key][:total_len] + + if global_cred_dict == {}: + msg = 'Not found any global credentials to perform discovery' + self.log(msg, "WARNING") - self.result.update(dict(credential_ids=self.creds_ids_list)) + return global_cred_dict def get_devices_list_info(self): """ @@ -631,7 +887,7 @@ def preprocess_device_discovery_handle_error(self): def discovery_specific_cred_failure(self, msg=None): """ - Method for failing discovery if there is any discrepancy in the http credentials + Method for failing discovery if there is any discrepancy in the credentials passed by the user """ @@ -767,7 +1023,7 @@ def handle_discovery_specific_credentials(self, new_object_params=None): return new_object_params - def create_params(self, credential_ids=None, ip_address_list=None): + def create_params(self, ip_address_list=None): """ Create a new parameter object based on the validated configuration, credential IDs, and IP address list. @@ -783,13 +1039,11 @@ def create_params(self, credential_ids=None, ip_address_list=None): parameters. """ - if credential_ids is None: - credential_ids = [] + 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['globalCredentialIdList'] = credential_ids 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') @@ -802,11 +1056,27 @@ def create_params(self, credential_ids=None, ip_address_list=None): if self.validated_config[0].get('discovery_specific_credentials'): self.handle_discovery_specific_credentials(new_object_params=new_object_params) + global_cred_dict = self.get_ccc_global_credentials_v2_info() + + self.log(global_cred_dict, "DEBUG") + if not (new_object_params.get('snmpUserName') or new_object_params.get('snmpROCommunityDesc') or new_object_params.get('snmpRWCommunityDesc') + or global_cred_dict.get('snmpV2cRead') or global_cred_dict.get('snmpV2cWrite') or global_cred_dict.get('snmpV3')): + msg = "Please provide atleast one valid SNMP credential to perform Discovery" + self.discovery_specific_cred_failure(msg=msg) + + if not (new_object_params.get('userNameList') or global_cred_dict.get('cliCredential')): + msg = "Please provide atleast one valid CLI credential to perform Discovery" + self.discovery_specific_cred_failure(msg=msg) + + for global_cred_list in global_cred_dict.values(): + credential_ids.extend(global_cred_list) + + new_object_params['globalCredentialIdList'] = credential_ids 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): + def create_discovery(self, 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 @@ -823,14 +1093,10 @@ def create_discovery(self, credential_ids=None, ip_address_list=None): - 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), + params=self.create_params(ip_address_list=ip_address_list), op_modifies=True, ) @@ -873,7 +1139,7 @@ def get_task_status(self, task_id=None): self.module.fail_json(msg=msg) return False - if response.get('progress') != 'In Progress': + if response.get('progress') != 'In Progress' or response.get('progress') != 'Inventory service initiating discovery': result = True self.log("The Process is completed", "INFO") break @@ -1079,7 +1345,6 @@ def get_diff_merged(self): - 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() @@ -1089,7 +1354,6 @@ def get_diff_merged(self): 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() From d0e2ebc3c5d66137387a762226fe3ab9be700553 Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 29 Feb 2024 19:26:41 +0000 Subject: [PATCH 18/44] Adding option to specify global credentials while performing discovery --- playbooks/discovery_intent.yml | 2 +- playbooks/discovery_workflow_manager.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index 10e79aa71d..a59de918a2 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -78,7 +78,7 @@ snmp_v2_read_credential: desc: string community: string - snmp_v2_write_credential: + snmp_v2_write_credential: desc: string community: string snmp_v3_credential: diff --git a/playbooks/discovery_workflow_manager.yml b/playbooks/discovery_workflow_manager.yml index 2eea576907..45d3b76207 100644 --- a/playbooks/discovery_workflow_manager.yml +++ b/playbooks/discovery_workflow_manager.yml @@ -78,7 +78,7 @@ snmp_v2_read_credential: desc: string community: string - snmp_v2_write_credential: + snmp_v2_write_credential: desc: string community: string snmp_v3_credential: From d34939db90b5d88b3dde364a5d240183979b15f4 Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 1 Mar 2024 18:36:52 +0000 Subject: [PATCH 19/44] Adding option to specify global credentials while performing discovery --- playbooks/discovery_intent.yml | 34 ++-- playbooks/discovery_workflow_manager.yml | 34 ++-- plugins/modules/discovery_intent.py | 174 +++++++++--------- plugins/modules/discovery_workflow_manager.py | 174 +++++++++--------- 4 files changed, 216 insertions(+), 200 deletions(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index a59de918a2..4837b49efd 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -27,7 +27,9 @@ state: merged config_verify: True config: - - ip_address_list: + - discovery_name: Multi_global + discovery_type: MULTI RANGE + ip_address_list: - 204.1.2.1-204.1.2.5 - 204.192.3.40 - 204.192.4.200 @@ -37,10 +39,6 @@ - 204.1.2.9 - 204.1.2.10 - 204.1.2.11 - discovery_type: MULTI RANGE - protocol_order: ssh - state: merged - discovery_name: Multi_global global_credentials: cli_credentials_list: - description: ISE @@ -53,6 +51,7 @@ snmp_v3_credential_list: - description: snmpV3 username: snmpV3 + protocol_order: ssh - name: Execute discovery of single device using various discovery specific credentials cisco.dnac.discovery_intent: @@ -64,7 +63,6 @@ discovery_type: "SINGLE" ip_address_list: - 204.1.2.5 - protocol_order: ssh discovery_specific_credentials: cli_credentials_list: - username: cisco @@ -89,6 +87,7 @@ privacy_type: AES256 privacy_password: Lablab#1234 global_cli_len: 3 + protocol_order: ssh - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials cisco.dnac.discovery_intent: @@ -96,7 +95,9 @@ state: merged config_verify: True config: - - ip_address_list: + - discovery_type: "MULTI RANGE" + discovery_name: Multi_range + 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: @@ -121,13 +122,10 @@ 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_intent: @@ -135,11 +133,11 @@ state: merged config_verify: True config: - - ip_address_list: #List length should be one - - 204.1.2.1 + - discovery_name: CDP_Test_1 discovery_type: "CDP" #Can be LLDP and CIDR - cdp_level: 2 #Instead use lldp for LLDP and prefix length for CIDR - discovery_name: CDP_Test_1 + ip_address_list: #List length should be one + - 204.1.2.1 + cdp_level: 2 #Instead use lldp_level for LLDP and prefix length for CIDR discovery_specific_credentials: cli_credentials_list: - username: admin @@ -147,6 +145,14 @@ enable_password: maglev123 protocol_order: ssh + - name: Execute deletion of single discovery from the dashboard + cisco.dnac.discovery_intent: + <<: *dnac_login + state: deleted + config_verify: True + config: + - discovery_name: CDP_Test_1 + - name: Execute deletion of all the discoveries from the dashboard cisco.dnac.discovery_intent: <<: *dnac_login diff --git a/playbooks/discovery_workflow_manager.yml b/playbooks/discovery_workflow_manager.yml index 45d3b76207..32b3590f05 100644 --- a/playbooks/discovery_workflow_manager.yml +++ b/playbooks/discovery_workflow_manager.yml @@ -27,7 +27,9 @@ state: merged config_verify: True config: - - ip_address_list: + - discovery_name: Multi_global + discovery_type: MULTI RANGE + ip_address_list: - 204.1.2.1-204.1.2.5 - 204.192.3.40 - 204.192.4.200 @@ -37,10 +39,6 @@ - 204.1.2.9 - 204.1.2.10 - 204.1.2.11 - discovery_type: MULTI RANGE - protocol_order: ssh - state: merged - discovery_name: Multi_global global_credentials: cli_credentials_list: - description: ISE @@ -53,6 +51,7 @@ snmp_v3_credential_list: - description: snmpV3 username: snmpV3 + protocol_order: ssh - name: Execute discovery of single device using various discovery specific credentials cisco.dnac.discovery_workflow_manager: @@ -64,7 +63,6 @@ discovery_type: "SINGLE" ip_address_list: - 204.1.2.5 - protocol_order: ssh discovery_specific_credentials: cli_credentials_list: - username: cisco @@ -89,6 +87,7 @@ privacy_type: AES256 privacy_password: Lablab#1234 global_cli_len: 3 + protocol_order: ssh - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials cisco.dnac.discovery_workflow_manager: @@ -96,7 +95,9 @@ state: merged config_verify: True config: - - ip_address_list: + - discovery_type: "MULTI RANGE" + discovery_name: Multi_range + 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: @@ -121,13 +122,10 @@ 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: @@ -135,11 +133,11 @@ state: merged config_verify: True config: - - ip_address_list: #List length should be one - - 204.1.2.1 + - discovery_name: CDP_Test_1 discovery_type: "CDP" #Can be LLDP and CIDR - cdp_level: 2 #Instead use lldp for LLDP and prefix length for CIDR - discovery_name: CDP_Test_1 + ip_address_list: #List length should be one + - 204.1.2.1 + cdp_level: 2 #Instead use lldp_level for LLDP and prefix length for CIDR discovery_specific_credentials: cli_credentials_list: - username: admin @@ -147,6 +145,14 @@ enable_password: maglev123 protocol_order: ssh + - name: Execute deletion of single discovery from the dashboard + cisco.dnac.discovery_workflow_manager: + <<: *dnac_login + state: deleted + config_verify: True + config: + - discovery_name: CDP_Test_1 + - name: Execute deletion of all the discoveries from the dashboard cisco.dnac.discovery_workflow_manager: <<: *dnac_login diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index bc07237292..2067bc9263 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -39,14 +39,9 @@ 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 + discovery_name: + description: Name of the discovery task + type: str required: true discovery_type: description: Determines the method of device discovery. Here are the available options. @@ -59,6 +54,19 @@ type: str required: true choices: [ 'SINGLE', 'RANGE', 'MULTI RANGE', 'CDP', 'LLDP', 'CIDR'] + 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 + ip_filter_list: + description: List of IP adddrsess that needs to get filtered out from the IP addresses passed. + type: list + elements: str cdp_level: description: Total number of levels that are there in cdp's method of discovery type: int @@ -67,14 +75,10 @@ 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 - records_to_return: - description: Number of records to return for the header in fetching global v2 credentials - type: int - default: 100 + preferred_mgmt_ip_method: + description: Preferred method for the management of the IP (None/UseLoopBack) + type: str + default: None discovery_specific_credentials: description: Credentials specifically created by the user for performing device discovery. type: dict @@ -203,8 +207,8 @@ global_credentials: description: - Credentials that are already created by the user under Device Credentials in Cisco Catalyst Center. - - If user doesn't pass any global credentials in the playbook. - - By default we will use all the global credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) + - If user doesn't pass any global credentials in the playbook, then by default, we will use all the global + credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) type: dict suboptions: cli_credentials_list: @@ -295,24 +299,20 @@ description: description: Name of the Net Conf Port credential, mandatory when using global Net conf port. type: str - ip_filter_list: - 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 - type: str - required: true - preferred_mgmt_ip_method: - description: Preferred method for the management of the IP (None/UseLoopBack) - type: str - default: None + start_index: + description: Start index for the header in fetching SNMP v2 credentials + type: int + default: 1 + records_to_return: + description: Number of records to return for the header in fetching global v2 credentials + type: int + default: 100 protocol_order: 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 + default: ssh retry: description: Number of times to try establishing connection to device type: int @@ -364,19 +364,13 @@ state: merged config_verify: True config: - - ip_address_list: list + - discovery_name: string discovery_type: string + ip_address_list: list + ip_filter_list: list cdp_level: string lldp_level: string - start_index: integer - records_to_return: integer - ip_filter_list: list - discovery_name: string - password_list: list prefered_mgmt_ip_method: string - protocol_order: string - retry: integer - timeout: integer discovery_specific_credentials: cli_credentials_list: - username: string @@ -427,6 +421,11 @@ username: string net_conf_port_list: - description: string + start_index: integer + records_to_return: integer + protocol_order: string + retry: integer + timeout: integer - name: Delete disovery by name cisco.dnac.discovery_intent: @@ -555,14 +554,15 @@ def validate_input(self, state=None): 'default': 'None'}, 'retry': {'type': 'int', 'required': False}, 'timeout': {'type': 'str', 'required': False}, - 'global_credentials': {'type': 'dict', 'required': False} + 'global_credentials': {'type': 'dict', 'required': False}, + 'protocol_order': {'type': 'str', 'required': False, + 'default': 'ssh'} } 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: @@ -610,7 +610,7 @@ def handle_global_credentials(self, response=None): - response: The response collected from the get_all_global_credentials_v2 API Returns: - - global_cred_dict : The dictionary containing list of IDs of various types of + - global_credentails_all : The dictionary containing list of IDs of various types of Global credentials. """ @@ -622,13 +622,13 @@ def handle_global_credentials(self, response=None): snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') net_conf_port_list = global_credentials.get('net_conf_port_list') - global_cred_dict = {} + global_credentails_all = {} if cli_credentials_list: if not isinstance(cli_credentials_list, list): msg = "Global ClI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: - global_cred_dict["cliCredential"] = [] + global_credentails_all["cliCredential"] = [] cred_len = len(cli_credentials_list) if cred_len > 5: cred_len = 5 @@ -636,10 +636,10 @@ def handle_global_credentials(self, response=None): if cli_cred.get('description') and cli_cred.get('username'): for cli in response.get("cliCredential"): if cli.get("description") == cli_cred.get('description') and cli.get("username") == cli_cred.get('username'): - global_cred_dict["cliCredential"].append(cli.get("id")) - global_cred_dict["cliCredential"] = global_cred_dict["cliCredential"][:cred_len] + global_credentails_all["cliCredential"].append(cli.get("id")) + global_credentails_all["cliCredential"] = global_credentails_all["cliCredential"][:cred_len] else: - msg = "Please provide description and username of the Global CLI credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global CLI credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if http_read_credential_list: @@ -647,7 +647,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(http_read_credential_list) > 0: - global_cred_dict["httpsRead"] = [] + global_credentails_all["httpsRead"] = [] cred_len = len(http_read_credential_list) if cred_len > 5: cred_len = 5 @@ -655,10 +655,10 @@ def handle_global_credentials(self, response=None): if http_cred.get('description') and http_cred.get('username'): for http in response.get("httpsRead"): if http.get("description") == http.get('description') and http.get("username") == http.get('username'): - global_cred_dict["httpsRead"].append(http.get("id")) - global_cred_dict["httpsRead"] = global_cred_dict["httpsRead"][:cred_len] + global_credentails_all["httpsRead"].append(http.get("id")) + global_credentails_all["httpsRead"] = global_credentails_all["httpsRead"][:cred_len] else: - msg = "Please provide description and username of the Global HTTP read credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global HTTP Read credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if http_write_credential_list: @@ -666,7 +666,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(http_write_credential_list) > 0: - global_cred_dict["httpsWrite"] = [] + global_credentails_all["httpsWrite"] = [] cred_len = len(http_write_credential_list) if cred_len > 5: cred_len = 5 @@ -674,10 +674,10 @@ def handle_global_credentials(self, response=None): if http_cred.get('description') and http_cred.get('username'): for http in response.get("httpsWrite"): if http.get("description") == http.get('description') and http.get("username") == http.get('username'): - global_cred_dict["httpsWrite"].append(http.get("id")) - global_cred_dict["httpsWrite"] = global_cred_dict["httpsWrite"][:cred_len] + global_credentails_all["httpsWrite"].append(http.get("id")) + global_credentails_all["httpsWrite"] = global_credentails_all["httpsWrite"][:cred_len] else: - msg = "Please provide description and username of the Global HTTP write credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global HTTP Write credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if snmp_v2_read_credential_list: @@ -685,7 +685,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: - global_cred_dict["snmpV2cRead"] = [] + global_credentails_all["snmpV2cRead"] = [] cred_len = len(snmp_v2_read_credential_list) if cred_len > 5: cred_len = 5 @@ -693,10 +693,11 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description'): for snmp in response.get("snmpV2cRead"): if snmp.get("description") == snmp_cred.get('description'): - global_cred_dict["snmpV2cRead"].append(snmp.get("id")) - global_cred_dict["snmpV2cRead"] = global_cred_dict["snmpV2cRead"][:cred_len] + global_credentails_all["snmpV2cRead"].append(snmp.get("id")) + global_credentails_all["snmpV2cRead"] = global_credentails_all["snmpV2cRead"][:cred_len] else: - msg = "Please provide description of the Global SNMPV2 read credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 Read \ + credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if snmp_v3_credential_list: @@ -704,7 +705,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV3 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v3_credential_list) > 0: - global_cred_dict["snmpV3"] = [] + global_credentails_all["snmpV3"] = [] cred_len = len(snmp_v3_credential_list) if cred_len > 5: cred_len = 5 @@ -712,10 +713,11 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description') and snmp_cred.get('username'): for snmp in response.get("snmpV3"): if snmp.get("description") == snmp_cred.get('description') and snmp.get("username") == snmp_cred.get('username'): - global_cred_dict["snmpV3"].append(snmp.get("id")) - global_cred_dict["snmpV3"] = global_cred_dict["snmpV3"][:cred_len] + global_credentails_all["snmpV3"].append(snmp.get("id")) + global_credentails_all["snmpV3"] = global_credentails_all["snmpV3"][:cred_len] else: - msg = "Please provide description and username of the Global SNMPV3 credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global SNMPV3 \ + to discover the devices" self.discovery_specific_cred_failure(msg=msg) if snmp_v2_write_credential_list: @@ -723,7 +725,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV2 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_write_credential_list) > 0: - global_cred_dict["snmpV2cWrite"] = [] + global_credentails_all["snmpV2cWrite"] = [] cred_len = len(snmp_v2_write_credential_list) if cred_len > 5: cred_len = 5 @@ -731,10 +733,10 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description'): for snmp in response.get("snmpV2cWrite"): if snmp.get("description") == snmp_cred.get('description'): - global_cred_dict["snmpV2cWrite"].append(snmp.get("id")) - global_cred_dict["snmpV2cWrite"] = global_cred_dict["snmpV2cWrite"][:cred_len] + global_credentails_all["snmpV2cWrite"].append(snmp.get("id")) + global_credentails_all["snmpV2cWrite"] = global_credentails_all["snmpV2cWrite"][:cred_len] else: - msg = "Please provide description of the Global SNMPV2 write credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if net_conf_port_list: @@ -742,7 +744,7 @@ def handle_global_credentials(self, response=None): msg = "Global net Conf Ports be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(net_conf_port_list) > 0: - global_cred_dict["netconfCredential"] = [] + global_credentails_all["netconfCredential"] = [] cred_len = len(net_conf_port_list) if cred_len > 5: cred_len = 5 @@ -750,14 +752,14 @@ def handle_global_credentials(self, response=None): if port.get("description"): for netconf in response.get("netconfCredential"): if port.get('description') == netconf.get('description'): - global_cred_dict["netconfCredential"].append(netconf.get("id")) - global_cred_dict["netconfCredential"] = global_cred_dict["netconfCredential"][:cred_len] + global_credentails_all["netconfCredential"].append(netconf.get("id")) + global_credentails_all["netconfCredential"] = global_credentails_all["netconfCredential"][:cred_len] else: msg = "Please provide description of the Global Netconf port to be used" self.discovery_specific_cred_failure(msg=msg) - self.log("Fetched Global credentials IDs are {0}".format(global_cred_dict), "INFO") - return global_cred_dict + self.log("Fetched Global credentials IDs are {0}".format(global_credentails_all), "INFO") + return global_credentails_all def get_ccc_global_credentials_v2_info(self): """ @@ -780,31 +782,31 @@ 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") - global_cred_dict = {} + global_credentails_all = {} global_credentials = self.validated_config[0].get("global_credentials") if global_credentials: - global_cred_dict = self.handle_global_credentials(response=response) + global_credentails_all = self.handle_global_credentials(response=response) - global_cred_set = set(global_cred_dict.keys()) + global_cred_set = set(global_credentails_all.keys()) response_cred_set = set(response.keys()) diff_keys = response_cred_set.difference(global_cred_set) for key in diff_keys: - global_cred_dict[key] = [] + global_credentails_all[key] = [] if response[key] is None: response[key] = [] total_len = len(response[key]) if total_len > 5: total_len = 5 for element in response.get(key): - global_cred_dict[key].append(element.get('id')) - global_cred_dict[key] = global_cred_dict[key][:total_len] + global_credentails_all[key].append(element.get('id')) + global_credentails_all[key] = global_credentails_all[key][:total_len] - if global_cred_dict == {}: + if global_credentails_all == {}: msg = 'Not found any global credentials to perform discovery' self.log(msg, "WARNING") - return global_cred_dict + return global_credentails_all def get_devices_list_info(self): """ @@ -1056,19 +1058,19 @@ def create_params(self, ip_address_list=None): if self.validated_config[0].get('discovery_specific_credentials'): self.handle_discovery_specific_credentials(new_object_params=new_object_params) - global_cred_dict = self.get_ccc_global_credentials_v2_info() + global_credentails_all = self.get_ccc_global_credentials_v2_info() - self.log(global_cred_dict, "DEBUG") + self.log(global_credentails_all, "DEBUG") if not (new_object_params.get('snmpUserName') or new_object_params.get('snmpROCommunityDesc') or new_object_params.get('snmpRWCommunityDesc') - or global_cred_dict.get('snmpV2cRead') or global_cred_dict.get('snmpV2cWrite') or global_cred_dict.get('snmpV3')): + or global_credentails_all.get('snmpV2cRead') or global_credentails_all.get('snmpV2cWrite') or global_credentails_all.get('snmpV3')): msg = "Please provide atleast one valid SNMP credential to perform Discovery" self.discovery_specific_cred_failure(msg=msg) - if not (new_object_params.get('userNameList') or global_cred_dict.get('cliCredential')): + if not (new_object_params.get('userNameList') or global_credentails_all.get('cliCredential')): msg = "Please provide atleast one valid CLI credential to perform Discovery" self.discovery_specific_cred_failure(msg=msg) - for global_cred_list in global_cred_dict.values(): + for global_cred_list in global_credentails_all.values(): credential_ids.extend(global_cred_list) new_object_params['globalCredentialIdList'] = credential_ids diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index baed92e207..9fb34797ca 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -39,14 +39,9 @@ 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 + discovery_name: + description: Name of the discovery task + type: str required: true discovery_type: description: Determines the method of device discovery. Here are the available options. @@ -59,6 +54,19 @@ type: str required: true choices: [ 'SINGLE', 'RANGE', 'MULTI RANGE', 'CDP', 'LLDP', 'CIDR'] + 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 + ip_filter_list: + description: List of IP adddrsess that needs to get filtered out from the IP addresses passed. + type: list + elements: str cdp_level: description: Total number of levels that are there in cdp's method of discovery type: int @@ -67,14 +75,10 @@ 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 - records_to_return: - description: Number of records to return for the header in fetching global v2 credentials - type: int - default: 100 + preferred_mgmt_ip_method: + description: Preferred method for the management of the IP (None/UseLoopBack) + type: str + default: None discovery_specific_credentials: description: Credentials specifically created by the user for performing device discovery. type: dict @@ -203,8 +207,8 @@ global_credentials: description: - Credentials that are already created by the user under Device Credentials in Cisco Catalyst Center. - - If user doesn't pass any global credentials in the playbook. - - By default we will use all the global credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) + - If user doesn't pass any global credentials in the playbook, then by default, we will use all the global + credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) type: dict suboptions: cli_credentials_list: @@ -295,24 +299,20 @@ description: description: Name of the Net Conf Port credential, mandatory when using global Net conf port. type: str - ip_filter_list: - 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 - type: str - required: true - preferred_mgmt_ip_method: - description: Preferred method for the management of the IP (None/UseLoopBack) - type: str - default: None + start_index: + description: Start index for the header in fetching SNMP v2 credentials + type: int + default: 1 + records_to_return: + description: Number of records to return for the header in fetching global v2 credentials + type: int + default: 100 protocol_order: 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 + default: ssh retry: description: Number of times to try establishing connection to device type: int @@ -364,19 +364,13 @@ state: merged config_verify: True config: - - ip_address_list: list + - discovery_name: string discovery_type: string + ip_address_list: list + ip_filter_list: list cdp_level: string lldp_level: string - start_index: integer - records_to_return: integer - ip_filter_list: list - discovery_name: string - password_list: list prefered_mgmt_ip_method: string - protocol_order: string - retry: integer - timeout: integer discovery_specific_credentials: cli_credentials_list: - username: string @@ -427,6 +421,11 @@ username: string net_conf_port_list: - description: string + start_index: integer + records_to_return: integer + protocol_order: string + retry: integer + timeout: integer - name: Delete disovery by name cisco.dnac.discovery_workflow_manager: @@ -555,14 +554,15 @@ def validate_input(self, state=None): 'default': 'None'}, 'retry': {'type': 'int', 'required': False}, 'timeout': {'type': 'str', 'required': False}, - 'global_credentials': {'type': 'dict', 'required': False} + 'global_credentials': {'type': 'dict', 'required': False}, + 'protocol_order': {'type': 'str', 'required': False, + 'default': 'ssh'} } 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: @@ -610,7 +610,7 @@ def handle_global_credentials(self, response=None): - response: The response collected from the get_all_global_credentials_v2 API Returns: - - global_cred_dict : The dictionary containing list of IDs of various types of + - global_credentails_all : The dictionary containing list of IDs of various types of Global credentials. """ @@ -622,13 +622,13 @@ def handle_global_credentials(self, response=None): snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') net_conf_port_list = global_credentials.get('net_conf_port_list') - global_cred_dict = {} + global_credentails_all = {} if cli_credentials_list: if not isinstance(cli_credentials_list, list): msg = "Global ClI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: - global_cred_dict["cliCredential"] = [] + global_credentails_all["cliCredential"] = [] cred_len = len(cli_credentials_list) if cred_len > 5: cred_len = 5 @@ -636,10 +636,10 @@ def handle_global_credentials(self, response=None): if cli_cred.get('description') and cli_cred.get('username'): for cli in response.get("cliCredential"): if cli.get("description") == cli_cred.get('description') and cli.get("username") == cli_cred.get('username'): - global_cred_dict["cliCredential"].append(cli.get("id")) - global_cred_dict["cliCredential"] = global_cred_dict["cliCredential"][:cred_len] + global_credentails_all["cliCredential"].append(cli.get("id")) + global_credentails_all["cliCredential"] = global_credentails_all["cliCredential"][:cred_len] else: - msg = "Please provide description and username of the Global CLI credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global CLI credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if http_read_credential_list: @@ -647,7 +647,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(http_read_credential_list) > 0: - global_cred_dict["httpsRead"] = [] + global_credentails_all["httpsRead"] = [] cred_len = len(http_read_credential_list) if cred_len > 5: cred_len = 5 @@ -655,10 +655,10 @@ def handle_global_credentials(self, response=None): if http_cred.get('description') and http_cred.get('username'): for http in response.get("httpsRead"): if http.get("description") == http.get('description') and http.get("username") == http.get('username'): - global_cred_dict["httpsRead"].append(http.get("id")) - global_cred_dict["httpsRead"] = global_cred_dict["httpsRead"][:cred_len] + global_credentails_all["httpsRead"].append(http.get("id")) + global_credentails_all["httpsRead"] = global_credentails_all["httpsRead"][:cred_len] else: - msg = "Please provide description and username of the Global HTTP read credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global HTTP Read credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if http_write_credential_list: @@ -666,7 +666,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(http_write_credential_list) > 0: - global_cred_dict["httpsWrite"] = [] + global_credentails_all["httpsWrite"] = [] cred_len = len(http_write_credential_list) if cred_len > 5: cred_len = 5 @@ -674,10 +674,10 @@ def handle_global_credentials(self, response=None): if http_cred.get('description') and http_cred.get('username'): for http in response.get("httpsWrite"): if http.get("description") == http.get('description') and http.get("username") == http.get('username'): - global_cred_dict["httpsWrite"].append(http.get("id")) - global_cred_dict["httpsWrite"] = global_cred_dict["httpsWrite"][:cred_len] + global_credentails_all["httpsWrite"].append(http.get("id")) + global_credentails_all["httpsWrite"] = global_credentails_all["httpsWrite"][:cred_len] else: - msg = "Please provide description and username of the Global HTTP write credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global HTTP Write credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if snmp_v2_read_credential_list: @@ -685,7 +685,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: - global_cred_dict["snmpV2cRead"] = [] + global_credentails_all["snmpV2cRead"] = [] cred_len = len(snmp_v2_read_credential_list) if cred_len > 5: cred_len = 5 @@ -693,10 +693,11 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description'): for snmp in response.get("snmpV2cRead"): if snmp.get("description") == snmp_cred.get('description'): - global_cred_dict["snmpV2cRead"].append(snmp.get("id")) - global_cred_dict["snmpV2cRead"] = global_cred_dict["snmpV2cRead"][:cred_len] + global_credentails_all["snmpV2cRead"].append(snmp.get("id")) + global_credentails_all["snmpV2cRead"] = global_credentails_all["snmpV2cRead"][:cred_len] else: - msg = "Please provide description of the Global SNMPV2 read credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 Read \ + credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if snmp_v3_credential_list: @@ -704,7 +705,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV3 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v3_credential_list) > 0: - global_cred_dict["snmpV3"] = [] + global_credentails_all["snmpV3"] = [] cred_len = len(snmp_v3_credential_list) if cred_len > 5: cred_len = 5 @@ -712,10 +713,11 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description') and snmp_cred.get('username'): for snmp in response.get("snmpV3"): if snmp.get("description") == snmp_cred.get('description') and snmp.get("username") == snmp_cred.get('username'): - global_cred_dict["snmpV3"].append(snmp.get("id")) - global_cred_dict["snmpV3"] = global_cred_dict["snmpV3"][:cred_len] + global_credentails_all["snmpV3"].append(snmp.get("id")) + global_credentails_all["snmpV3"] = global_credentails_all["snmpV3"][:cred_len] else: - msg = "Please provide description and username of the Global SNMPV3 credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global SNMPV3 \ + to discover the devices" self.discovery_specific_cred_failure(msg=msg) if snmp_v2_write_credential_list: @@ -723,7 +725,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV2 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_write_credential_list) > 0: - global_cred_dict["snmpV2cWrite"] = [] + global_credentails_all["snmpV2cWrite"] = [] cred_len = len(snmp_v2_write_credential_list) if cred_len > 5: cred_len = 5 @@ -731,10 +733,10 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description'): for snmp in response.get("snmpV2cWrite"): if snmp.get("description") == snmp_cred.get('description'): - global_cred_dict["snmpV2cWrite"].append(snmp.get("id")) - global_cred_dict["snmpV2cWrite"] = global_cred_dict["snmpV2cWrite"][:cred_len] + global_credentails_all["snmpV2cWrite"].append(snmp.get("id")) + global_credentails_all["snmpV2cWrite"] = global_credentails_all["snmpV2cWrite"][:cred_len] else: - msg = "Please provide description of the Global SNMPV2 write credential to be used" + msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) if net_conf_port_list: @@ -742,7 +744,7 @@ def handle_global_credentials(self, response=None): msg = "Global net Conf Ports be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(net_conf_port_list) > 0: - global_cred_dict["netconfCredential"] = [] + global_credentails_all["netconfCredential"] = [] cred_len = len(net_conf_port_list) if cred_len > 5: cred_len = 5 @@ -750,14 +752,14 @@ def handle_global_credentials(self, response=None): if port.get("description"): for netconf in response.get("netconfCredential"): if port.get('description') == netconf.get('description'): - global_cred_dict["netconfCredential"].append(netconf.get("id")) - global_cred_dict["netconfCredential"] = global_cred_dict["netconfCredential"][:cred_len] + global_credentails_all["netconfCredential"].append(netconf.get("id")) + global_credentails_all["netconfCredential"] = global_credentails_all["netconfCredential"][:cred_len] else: msg = "Please provide description of the Global Netconf port to be used" self.discovery_specific_cred_failure(msg=msg) - self.log("Fetched Global credentials IDs are {0}".format(global_cred_dict), "INFO") - return global_cred_dict + self.log("Fetched Global credentials IDs are {0}".format(global_credentails_all), "INFO") + return global_credentails_all def get_ccc_global_credentials_v2_info(self): """ @@ -780,31 +782,31 @@ 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") - global_cred_dict = {} + global_credentails_all = {} global_credentials = self.validated_config[0].get("global_credentials") if global_credentials: - global_cred_dict = self.handle_global_credentials(response=response) + global_credentails_all = self.handle_global_credentials(response=response) - global_cred_set = set(global_cred_dict.keys()) + global_cred_set = set(global_credentails_all.keys()) response_cred_set = set(response.keys()) diff_keys = response_cred_set.difference(global_cred_set) for key in diff_keys: - global_cred_dict[key] = [] + global_credentails_all[key] = [] if response[key] is None: response[key] = [] total_len = len(response[key]) if total_len > 5: total_len = 5 for element in response.get(key): - global_cred_dict[key].append(element.get('id')) - global_cred_dict[key] = global_cred_dict[key][:total_len] + global_credentails_all[key].append(element.get('id')) + global_credentails_all[key] = global_credentails_all[key][:total_len] - if global_cred_dict == {}: + if global_credentails_all == {}: msg = 'Not found any global credentials to perform discovery' self.log(msg, "WARNING") - return global_cred_dict + return global_credentails_all def get_devices_list_info(self): """ @@ -1056,19 +1058,19 @@ def create_params(self, ip_address_list=None): if self.validated_config[0].get('discovery_specific_credentials'): self.handle_discovery_specific_credentials(new_object_params=new_object_params) - global_cred_dict = self.get_ccc_global_credentials_v2_info() + global_credentails_all = self.get_ccc_global_credentials_v2_info() - self.log(global_cred_dict, "DEBUG") + self.log(global_credentails_all, "DEBUG") if not (new_object_params.get('snmpUserName') or new_object_params.get('snmpROCommunityDesc') or new_object_params.get('snmpRWCommunityDesc') - or global_cred_dict.get('snmpV2cRead') or global_cred_dict.get('snmpV2cWrite') or global_cred_dict.get('snmpV3')): + or global_credentails_all.get('snmpV2cRead') or global_credentails_all.get('snmpV2cWrite') or global_credentails_all.get('snmpV3')): msg = "Please provide atleast one valid SNMP credential to perform Discovery" self.discovery_specific_cred_failure(msg=msg) - if not (new_object_params.get('userNameList') or global_cred_dict.get('cliCredential')): + if not (new_object_params.get('userNameList') or global_credentails_all.get('cliCredential')): msg = "Please provide atleast one valid CLI credential to perform Discovery" self.discovery_specific_cred_failure(msg=msg) - for global_cred_list in global_cred_dict.values(): + for global_cred_list in global_credentails_all.values(): credential_ids.extend(global_cred_list) new_object_params['globalCredentialIdList'] = credential_ids From 57b7c820c4bdf0c37b823f1fbf70b1729c1e1c9d Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 1 Mar 2024 19:00:11 +0000 Subject: [PATCH 20/44] Adding option to specify global credentials while performing discovery --- plugins/modules/discovery_intent.py | 84 +++++++++---------- plugins/modules/discovery_workflow_manager.py | 83 +++++++++--------- 2 files changed, 84 insertions(+), 83 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 2067bc9263..8034337bb8 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -206,29 +206,29 @@ type: str global_credentials: description: - - Credentials that are already created by the user under Device Credentials in Cisco Catalyst Center. + - Set of various credential types, including CLI, SNMP, HTTP, and NETCONF, that a user has pre-configured in + the Device Credentials section of the Cisco Catalyst Center. - If user doesn't pass any global credentials in the playbook, then by default, we will use all the global credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) type: dict suboptions: cli_credentials_list: description: - - List of Global CLI credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - Accepts a list of global CLI credentials for use in device discovery. + - It's recommended to create device credentials with both a unique username and a clear description. type: list elements: dict suboptions: username: - description: Username for CLI authentication, mandatory when using global CLI credentials. + description: Username required for CLI authentication and is mandatory when using global CLI credentials. type: str description: description: Name of the CLI credential, mandatory when using global CLI credentials. type: str http_read_credential_list: description: - - List of Global HTTP Read credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. - type: list + - List of global HTTP Read credentials that will be used in the process of discovering devices. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. elements: dict suboptions: username: @@ -239,8 +239,8 @@ type: str http_write_credential_list: description: - - List of Global HTTP Write credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - List of global HTTP Write credentials that will be used in the process of discovering devices. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -253,7 +253,7 @@ snmp_v2_read_credential_list: description: - List of Global SNMP V2 Read credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -266,7 +266,7 @@ snmp_v2_write_credential_list: description: - List of Global SNMP V2 Write credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -279,7 +279,7 @@ snmp_v3_credential_list: description: - List of Global SNMP V3 credentials to be used during device discovery, giving read and write mode. - - Generally it is advised to create device credentials with unique username or description. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -292,7 +292,7 @@ net_conf_port_list: description: - List of Global Net conf ports to be used during device discovery. - - Generally it is advised to create device credentials with unique description. + - It's recommended to create device credentials with unique description. type: list elements: dict suboptions: @@ -555,8 +555,7 @@ def validate_input(self, state=None): 'retry': {'type': 'int', 'required': False}, 'timeout': {'type': 'str', 'required': False}, 'global_credentials': {'type': 'dict', 'required': False}, - 'protocol_order': {'type': 'str', 'required': False, - 'default': 'ssh'} + 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'} } if state == "merged": @@ -615,17 +614,12 @@ def handle_global_credentials(self, response=None): """ global_credentials = self.validated_config[0].get("global_credentials") - cli_credentials_list = global_credentials.get('cli_credentials_list') - http_read_credential_list = global_credentials.get('http_read_credential_list') - http_write_credential_list = global_credentials.get('http_write_credential_list') - snmp_v2_read_credential_list = global_credentials.get('snmp_v2_read_credential_list') - snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') - snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') - net_conf_port_list = global_credentials.get('net_conf_port_list') global_credentails_all = {} + + cli_credentials_list = global_credentials.get('cli_credentials_list') if cli_credentials_list: if not isinstance(cli_credentials_list, list): - msg = "Global ClI credentials must be passed as a list" + msg = "Global CLI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: global_credentails_all["cliCredential"] = [] @@ -642,6 +636,7 @@ def handle_global_credentials(self, response=None): msg = "Kindly ensure you include both the description and the username for the Global CLI credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) + http_read_credential_list = global_credentials.get('http_read_credential_list') if http_read_credential_list: if not isinstance(http_read_credential_list, list): msg = "Global HTTP read credentials must be passed as a list" @@ -661,6 +656,7 @@ def handle_global_credentials(self, response=None): msg = "Kindly ensure you include both the description and the username for the Global HTTP Read credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) + http_write_credential_list = global_credentials.get('http_write_credential_list') if http_write_credential_list: if not isinstance(http_write_credential_list, list): msg = "Global HTTP write credentials must be passed as a list" @@ -680,6 +676,7 @@ def handle_global_credentials(self, response=None): msg = "Kindly ensure you include both the description and the username for the Global HTTP Write credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) + snmp_v2_read_credential_list = global_credentials.get('snmp_v2_read_credential_list') if snmp_v2_read_credential_list: if not isinstance(snmp_v2_read_credential_list, list): msg = "Global SNMPV2 read credentials must be passed as a list" @@ -700,6 +697,27 @@ def handle_global_credentials(self, response=None): credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) + snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') + if snmp_v2_write_credential_list: + if not isinstance(snmp_v2_write_credential_list, list): + msg = "Global SNMPV2 write credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(snmp_v2_write_credential_list) > 0: + global_credentails_all["snmpV2cWrite"] = [] + cred_len = len(snmp_v2_write_credential_list) + if cred_len > 5: + cred_len = 5 + for snmp_cred in snmp_v2_write_credential_list: + if snmp_cred.get('description'): + for snmp in response.get("snmpV2cWrite"): + if snmp.get("description") == snmp_cred.get('description'): + global_credentails_all["snmpV2cWrite"].append(snmp.get("id")) + global_credentails_all["snmpV2cWrite"] = global_credentails_all["snmpV2cWrite"][:cred_len] + else: + msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" + self.discovery_specific_cred_failure(msg=msg) + + snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') if snmp_v3_credential_list: if not isinstance(snmp_v3_credential_list, list): msg = "Global SNMPV3 write credentials must be passed as a list" @@ -720,25 +738,7 @@ def handle_global_credentials(self, response=None): to discover the devices" self.discovery_specific_cred_failure(msg=msg) - if snmp_v2_write_credential_list: - if not isinstance(snmp_v2_write_credential_list, list): - msg = "Global SNMPV2 write credentials must be passed as a list" - self.discovery_specific_cred_failure(msg=msg) - if len(snmp_v2_write_credential_list) > 0: - global_credentails_all["snmpV2cWrite"] = [] - cred_len = len(snmp_v2_write_credential_list) - if cred_len > 5: - cred_len = 5 - for snmp_cred in snmp_v2_write_credential_list: - if snmp_cred.get('description'): - for snmp in response.get("snmpV2cWrite"): - if snmp.get("description") == snmp_cred.get('description'): - global_credentails_all["snmpV2cWrite"].append(snmp.get("id")) - global_credentails_all["snmpV2cWrite"] = global_credentails_all["snmpV2cWrite"][:cred_len] - else: - msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" - self.discovery_specific_cred_failure(msg=msg) - + net_conf_port_list = global_credentials.get('net_conf_port_list') if net_conf_port_list: if not isinstance(net_conf_port_list, list): msg = "Global net Conf Ports be passed as a list" diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 9fb34797ca..c61a3d9d9e 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -206,28 +206,29 @@ type: str global_credentials: description: - - Credentials that are already created by the user under Device Credentials in Cisco Catalyst Center. + - Set of various credential types, including CLI, SNMP, HTTP, and NETCONF, that a user has pre-configured in + the Device Credentials section of the Cisco Catalyst Center. - If user doesn't pass any global credentials in the playbook, then by default, we will use all the global credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) type: dict suboptions: cli_credentials_list: description: - - List of Global CLI credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - Accepts a list of global CLI credentials for use in device discovery. + - It's recommended to create device credentials with both a unique username and a clear description. type: list elements: dict suboptions: username: - description: Username for CLI authentication, mandatory when using global CLI credentials. + description: Username required for CLI authentication and is mandatory when using global CLI credentials. type: str description: description: Name of the CLI credential, mandatory when using global CLI credentials. type: str http_read_credential_list: description: - - List of Global HTTP Read credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - List of global HTTP Read credentials that will be used in the process of discovering devices. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -239,8 +240,8 @@ type: str http_write_credential_list: description: - - List of Global HTTP Write credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - List of global HTTP Write credentials that will be used in the process of discovering devices. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -253,7 +254,7 @@ snmp_v2_read_credential_list: description: - List of Global SNMP V2 Read credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -266,7 +267,7 @@ snmp_v2_write_credential_list: description: - List of Global SNMP V2 Write credentials to be used during device discovery. - - Generally it is advised to create device credentials with unique username or description. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -279,7 +280,7 @@ snmp_v3_credential_list: description: - List of Global SNMP V3 credentials to be used during device discovery, giving read and write mode. - - Generally it is advised to create device credentials with unique username or description. + - It's recommended to create device credentials with both a unique username and a clear description for easy identification. type: list elements: dict suboptions: @@ -292,7 +293,7 @@ net_conf_port_list: description: - List of Global Net conf ports to be used during device discovery. - - Generally it is advised to create device credentials with unique description. + - It's recommended to create device credentials with unique description. type: list elements: dict suboptions: @@ -555,8 +556,7 @@ def validate_input(self, state=None): 'retry': {'type': 'int', 'required': False}, 'timeout': {'type': 'str', 'required': False}, 'global_credentials': {'type': 'dict', 'required': False}, - 'protocol_order': {'type': 'str', 'required': False, - 'default': 'ssh'} + 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'} } if state == "merged": @@ -615,17 +615,12 @@ def handle_global_credentials(self, response=None): """ global_credentials = self.validated_config[0].get("global_credentials") - cli_credentials_list = global_credentials.get('cli_credentials_list') - http_read_credential_list = global_credentials.get('http_read_credential_list') - http_write_credential_list = global_credentials.get('http_write_credential_list') - snmp_v2_read_credential_list = global_credentials.get('snmp_v2_read_credential_list') - snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') - snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') - net_conf_port_list = global_credentials.get('net_conf_port_list') global_credentails_all = {} + + cli_credentials_list = global_credentials.get('cli_credentials_list') if cli_credentials_list: if not isinstance(cli_credentials_list, list): - msg = "Global ClI credentials must be passed as a list" + msg = "Global CLI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: global_credentails_all["cliCredential"] = [] @@ -642,6 +637,7 @@ def handle_global_credentials(self, response=None): msg = "Kindly ensure you include both the description and the username for the Global CLI credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) + http_read_credential_list = global_credentials.get('http_read_credential_list') if http_read_credential_list: if not isinstance(http_read_credential_list, list): msg = "Global HTTP read credentials must be passed as a list" @@ -661,6 +657,7 @@ def handle_global_credentials(self, response=None): msg = "Kindly ensure you include both the description and the username for the Global HTTP Read credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) + http_write_credential_list = global_credentials.get('http_write_credential_list') if http_write_credential_list: if not isinstance(http_write_credential_list, list): msg = "Global HTTP write credentials must be passed as a list" @@ -680,6 +677,7 @@ def handle_global_credentials(self, response=None): msg = "Kindly ensure you include both the description and the username for the Global HTTP Write credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) + snmp_v2_read_credential_list = global_credentials.get('snmp_v2_read_credential_list') if snmp_v2_read_credential_list: if not isinstance(snmp_v2_read_credential_list, list): msg = "Global SNMPV2 read credentials must be passed as a list" @@ -700,6 +698,27 @@ def handle_global_credentials(self, response=None): credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) + snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') + if snmp_v2_write_credential_list: + if not isinstance(snmp_v2_write_credential_list, list): + msg = "Global SNMPV2 write credentials must be passed as a list" + self.discovery_specific_cred_failure(msg=msg) + if len(snmp_v2_write_credential_list) > 0: + global_credentails_all["snmpV2cWrite"] = [] + cred_len = len(snmp_v2_write_credential_list) + if cred_len > 5: + cred_len = 5 + for snmp_cred in snmp_v2_write_credential_list: + if snmp_cred.get('description'): + for snmp in response.get("snmpV2cWrite"): + if snmp.get("description") == snmp_cred.get('description'): + global_credentails_all["snmpV2cWrite"].append(snmp.get("id")) + global_credentails_all["snmpV2cWrite"] = global_credentails_all["snmpV2cWrite"][:cred_len] + else: + msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" + self.discovery_specific_cred_failure(msg=msg) + + snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') if snmp_v3_credential_list: if not isinstance(snmp_v3_credential_list, list): msg = "Global SNMPV3 write credentials must be passed as a list" @@ -720,25 +739,7 @@ def handle_global_credentials(self, response=None): to discover the devices" self.discovery_specific_cred_failure(msg=msg) - if snmp_v2_write_credential_list: - if not isinstance(snmp_v2_write_credential_list, list): - msg = "Global SNMPV2 write credentials must be passed as a list" - self.discovery_specific_cred_failure(msg=msg) - if len(snmp_v2_write_credential_list) > 0: - global_credentails_all["snmpV2cWrite"] = [] - cred_len = len(snmp_v2_write_credential_list) - if cred_len > 5: - cred_len = 5 - for snmp_cred in snmp_v2_write_credential_list: - if snmp_cred.get('description'): - for snmp in response.get("snmpV2cWrite"): - if snmp.get("description") == snmp_cred.get('description'): - global_credentails_all["snmpV2cWrite"].append(snmp.get("id")) - global_credentails_all["snmpV2cWrite"] = global_credentails_all["snmpV2cWrite"][:cred_len] - else: - msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" - self.discovery_specific_cred_failure(msg=msg) - + net_conf_port_list = global_credentials.get('net_conf_port_list') if net_conf_port_list: if not isinstance(net_conf_port_list, list): msg = "Global net Conf Ports be passed as a list" From eeaf78e7f234707f9128df5218e6c658ed8a7a20 Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 1 Mar 2024 19:04:24 +0000 Subject: [PATCH 21/44] Adding option to specify global credentials while performing discovery --- plugins/modules/discovery_intent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 8034337bb8..ee7667ec20 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -229,6 +229,7 @@ description: - List of global HTTP Read credentials that will be used in the process of discovering devices. - It's recommended to create device credentials with both a unique username and a clear description for easy identification. + type: list elements: dict suboptions: username: From 7afd4e0fc7388d547b5ad0cfd509935f65df1e47 Mon Sep 17 00:00:00 2001 From: Madhan Date: Sat, 2 Mar 2024 01:22:28 +0530 Subject: [PATCH 22/44] Changes in discovery and inventory workflow modules --- changelogs/changelog.yaml | 7 +++++++ galaxy.yml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 9f2bdcfc31..d1f6b4cac2 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -806,3 +806,10 @@ releases: - 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. + 6.11.1: + release_date: "2024-03-02" + changes: + release_summary: Enhancements in discovery and inventory workflow manager modules. + minor_changes: + - Changes in discovery workflow manager module to support SNMP credentials v2 and handling error messages. + - Changes in inventory workflow manager module to support snmp v2. diff --git a/galaxy.yml b/galaxy.yml index f5b2697877..653b3cf22a 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.11.0 +version: 6.11.1 readme: README.md authors: - Rafael Campos From e04ccf70ee058a4b57115a1d7504681230baa6fa Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 4 Mar 2024 14:49:00 +0530 Subject: [PATCH 23/44] Add the option of giving device series name while performing the task of SWIM Image Distribution/Activation along with giving the name of site, device family and device role, check with regex for device series name, update examples and docs as well. --- playbooks/swim_workflow_manager.yml | 3 ++ plugins/modules/swim_intent.py | 56 ++++++++++++++++++------ plugins/modules/swim_workflow_manager.py | 54 +++++++++++++++++------ 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/playbooks/swim_workflow_manager.yml b/playbooks/swim_workflow_manager.yml index 0bd1ad2815..7682441d61 100644 --- a/playbooks/swim_workflow_manager.yml +++ b/playbooks/swim_workflow_manager.yml @@ -37,10 +37,13 @@ site_name: "{{item.site_name}}" device_role: "{{ item.device_role }}" device_family_name: "{{ item.device_family_name }}" + device_series_name: "Catalyst 9300 Series" image_activation_details: + image_name: "{{item.image_name}}" site_name: "{{item.site_name}}" device_role: "{{ item.device_role }}" device_family_name: "{{ item.device_family_name }}" + device_series_name: "Catalyst 9300 Series" scehdule_validate: False distribute_if_needed: True diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 1447a9a609..fad4f8d079 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -181,6 +181,10 @@ 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 + device_series_name: + description: This parameter specifies the name of the device series. It is used to identify a specific series of devices, + such as Cisco Catalyst 9300 Series Switches, within the Cisco Catalyst Center. + type: str tagging: description: Booelan value to tag/untag SWIM image as golden If True then the given image will be tagged as golden. @@ -215,6 +219,10 @@ site_name: description: Used to get device details associated to this site. type: str + device_series_name: + description: This parameter specifies the name of the device series. It is used to identify a specific series of devices, + such as Cisco Catalyst 9300 Series Switches, within the Cisco Catalyst Center. + type: str image_name: description: SWIM image's name type: str @@ -429,6 +437,7 @@ site_name: Global/USA/San Francisco/BGL_18 device_role: ALL device_family_name: Switches and Hubs + device_series_name: Cisco Catalyst 9300 Series Switches - name: Activate the given image on devices associated to that site with specified role. cisco.dnac.swim_intent: @@ -447,6 +456,7 @@ site_name: Global/USA/San Francisco/BGL_18 device_role: ALL device_family_name: Switches and Hubs + device_series_name: Cisco Catalyst 9300 Series Switches scehdule_validate: False activate_lower_image_version: True distribute_if_needed: True @@ -712,7 +722,7 @@ def get_device_id(self, params): return device_id - def get_device_uuids(self, site_name, device_family, device_role): + def get_device_uuids(self, site_name, device_family, device_role, device_series_name=None): """ Retrieve a list of device UUIDs based on the specified criteria. Parameters: @@ -720,6 +730,7 @@ def get_device_uuids(self, site_name, device_family, device_role): 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. + device_series_name(str): Specifies the name of the device series. Returns: list: A list of device UUIDs that match the specified criteria. Description: @@ -730,15 +741,25 @@ def get_device_uuids(self, site_name, device_family, device_role): device_uuid_list = [] if not site_name: + self.log("Site name is not given so cannot fetch the devices associated to site", "INFO") return device_uuid_list (site_exists, site_id) = self.site_exists(site_name) if not site_exists: + self.log("""Given site '{0}' is not present in Cisco Catalyst Center so cannot fetch the devices associated + to site""".format(site_name), "INFO") return device_uuid_list + if device_series_name: + if device_series_name.startswith(".*") and device_series_name.endswith(".*"): + self.log("Device series name '{0}' already present in the regex format".format(device_series_name), "INFO") + else: + device_series_name = ".*" + device_series_name + ".*" + site_params = { "site_id": site_id, - "device_family": device_family + "device_family": device_family, + "device_series_name": device_series_name } response = self.dnac._exec( family="sites", @@ -752,8 +773,12 @@ def get_device_uuids(self, site_name, device_family, device_role): if len(response) > 0: for item in response: if item["reachabilityStatus"] != "Reachable": + self.log("""Reachability status of device '{0}' is '{1}' so cannot add it for distribution/activation + task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") continue if "role" in item and (device_role is None or item["role"] == device_role.upper() or device_role.upper() == "ALL"): + self.log("""Successfully fetch the device '{0}' associated to the given site '{1}' for swim distribution/activation + task""".format(item["managementIpAddress"], site_name)) device_uuid_list.append(item["instanceUuid"]) return device_uuid_list @@ -874,8 +899,10 @@ def get_have(self): 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"): @@ -1250,7 +1277,8 @@ def get_diff_distribution(self): 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) + device_series_name = distribution_details.get("device_series_name") + device_uuid_list = self.get_device_uuids(site_name, device_family, device_role, device_series_name) image_id = self.have.get("distribution_image_id") if self.have.get("distribution_device_id"): @@ -1289,7 +1317,7 @@ def get_diff_distribution(self): 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.log(self.msg, "ERROR") self.result['response'] = task_details break @@ -1301,7 +1329,7 @@ def get_diff_distribution(self): 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") + self.log(self.msg, "ERROR") return self self.log("Device UUIDs involved in Image Distribution: {0}".format(str(device_uuid_list)), "INFO") @@ -1344,8 +1372,8 @@ def get_diff_distribution(self): break if task_details.get("isError"): - error_msg = "Image with Id '{0}' Distribution failed".format(image_id) - self.log(error_msg, "WARNING") + self.msg = "Image with Id '{0}' Distribution failed".format(image_id) + self.log(self.msg, "ERROR") self.result['response'] = task_details device_ips_list.append(device_management_ip) break @@ -1387,7 +1415,8 @@ def get_diff_activation(self): 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) + device_series_name = activation_details.get("device_series_name") + device_uuid_list = self.get_device_uuids(site_name, device_family, device_role, device_series_name) image_id = self.have.get("activation_image_id") if self.have.get("activation_device_id"): @@ -1429,11 +1458,10 @@ def get_diff_activation(self): break if task_details.get("isError"): - error_msg = "Activation for Image with Id '{0}' gets failed".format(image_id) + self.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") + self.log(self.msg, "ERROR") return self self.result['response'] = task_details if task_details else response @@ -1444,7 +1472,7 @@ def get_diff_activation(self): self.status = "failed" self.msg = "No devices found for Image Activation" self.result['msg'] = self.msg - self.log(self.msg, "WARNING") + self.log(self.msg, "ERROR") return self self.log("Device UUIDs involved in Image Activation: {0}".format(str(device_uuid_list)), "INFO") @@ -1493,8 +1521,8 @@ def get_diff_activation(self): break if task_details.get("isError"): - error_msg = "Image with Id '{0}' activation failed".format(image_id) - self.log(error_msg, "WARNING") + self.msg = "Image with Id '{0}' activation failed".format(image_id) + self.log(self.msg, "ERROR") self.result['response'] = task_details device_ips_list.append(device_management_ip) break diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 3594bdd315..7a59026c24 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -215,6 +215,10 @@ site_name: description: Used to get device details associated to this site. type: str + device_series_name: + description: This parameter specifies the name of the device series. It is used to identify a specific series of devices, + such as Cisco Catalyst 9300 Series Switches, within the Cisco Catalyst Center. + type: str image_name: description: SWIM image's name type: str @@ -246,6 +250,10 @@ site_name: description: Used to get device details associated to this site. type: str + device_series_name: + description: This parameter specifies the name of the device series. It is used to identify a specific series of devices, + such as Cisco Catalyst 9300 Series Switches, within the Cisco Catalyst Center. + type: str activate_lower_image_version: description: ActivateLowerImageVersion flag. type: bool @@ -416,6 +424,7 @@ site_name: Global/USA/San Francisco/BGL_18 device_role: ALL device_family_name: Switches and Hubs + device_series_name: Cisco Catalyst 9300 Series Switches - name: Activate the given image on devices associated to that site with specified role. cisco.dnac.swim_workflow_manager: @@ -434,6 +443,7 @@ site_name: Global/USA/San Francisco/BGL_18 device_role: ALL device_family_name: Switches and Hubs + device_series_name: Cisco Catalyst 9300 Series Switches scehdule_validate: False activate_lower_image_version: True distribute_if_needed: True @@ -698,7 +708,7 @@ def get_device_id(self, params): return device_id - def get_device_uuids(self, site_name, device_family, device_role): + def get_device_uuids(self, site_name, device_family, device_role, device_series_name=None): """ Retrieve a list of device UUIDs based on the specified criteria. Parameters: @@ -706,6 +716,7 @@ def get_device_uuids(self, site_name, device_family, device_role): 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. + device_series_name(str): Specifies the name of the device series. Returns: list: A list of device UUIDs that match the specified criteria. Description: @@ -716,15 +727,25 @@ def get_device_uuids(self, site_name, device_family, device_role): device_uuid_list = [] if not site_name: + self.log("Site name is not given so cannot fetch the devices associated to site", "INFO") return device_uuid_list (site_exists, site_id) = self.site_exists(site_name) if not site_exists: + self.log("""Given site '{0}' is not present in Cisco Catalyst Center so cannot fetch the devices associated + to site""".format(site_name), "INFO") return device_uuid_list + if device_series_name: + if device_series_name.startswith(".*") and device_series_name.endswith(".*"): + self.log("Device series name '{0}' already present in the regex format".format(device_series_name), "INFO") + else: + device_series_name = ".*" + device_series_name + ".*" + site_params = { "site_id": site_id, - "device_family": device_family + "device_family": device_family, + "device_series_name": device_series_name } response = self.dnac._exec( family="sites", @@ -738,8 +759,12 @@ def get_device_uuids(self, site_name, device_family, device_role): if len(response) > 0: for item in response: if item["reachabilityStatus"] != "Reachable": + self.log("""Reachability status of device '{0}' is '{1}' so cannot add it for distribution/activation + task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") continue if "role" in item and (device_role is None or item["role"] == device_role.upper() or device_role.upper() == "ALL"): + self.log("""Successfully fetch the device '{0}' associated to the given site '{1}' for swim distribution/activation + task""".format(item["managementIpAddress"], site_name)) device_uuid_list.append(item["instanceUuid"]) return device_uuid_list @@ -860,8 +885,10 @@ def get_have(self): 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"): @@ -1236,7 +1263,8 @@ def get_diff_distribution(self): 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) + device_series_name = distribution_details.get("device_series_name") + device_uuid_list = self.get_device_uuids(site_name, device_family, device_role, device_series_name) image_id = self.have.get("distribution_image_id") if self.have.get("distribution_device_id"): @@ -1275,7 +1303,7 @@ def get_diff_distribution(self): 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.log(self.msg, "ERROR") self.result['response'] = task_details break @@ -1287,7 +1315,7 @@ def get_diff_distribution(self): 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") + self.log(self.msg, "ERROR") return self self.log("Device UUIDs involved in Image Distribution: {0}".format(str(device_uuid_list)), "INFO") @@ -1331,7 +1359,7 @@ def get_diff_distribution(self): if task_details.get("isError"): error_msg = "Image with Id '{0}' Distribution failed".format(image_id) - self.log(error_msg, "WARNING") + self.log(error_msg, "ERROR") self.result['response'] = task_details device_ips_list.append(device_management_ip) break @@ -1373,7 +1401,8 @@ def get_diff_activation(self): 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) + device_series_name = activation_details.get("device_series_name") + device_uuid_list = self.get_device_uuids(site_name, device_family, device_role, device_series_name) image_id = self.have.get("activation_image_id") if self.have.get("activation_device_id"): @@ -1415,11 +1444,10 @@ def get_diff_activation(self): break if task_details.get("isError"): - error_msg = "Activation for Image with Id '{0}' gets failed".format(image_id) + self.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") + self.log(self.msg, "ERROR") return self self.result['response'] = task_details if task_details else response @@ -1430,7 +1458,7 @@ def get_diff_activation(self): self.status = "failed" self.msg = "No devices found for Image Activation" self.result['msg'] = self.msg - self.log(self.msg, "WARNING") + self.log(self.msg, "ERROR") return self self.log("Device UUIDs involved in Image Activation: {0}".format(str(device_uuid_list)), "INFO") @@ -1479,8 +1507,8 @@ def get_diff_activation(self): break if task_details.get("isError"): - error_msg = "Image with Id '{0}' activation failed".format(image_id) - self.log(error_msg, "WARNING") + self.msg = "Image with Id '{0}' activation failed".format(image_id) + self.log(self.msg, "ERROR") self.result['response'] = task_details device_ips_list.append(device_management_ip) break From 26f3760591d2d0debe52a3f437135bcc2a988f5b Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 4 Mar 2024 15:55:44 +0530 Subject: [PATCH 24/44] update log messages of swim module --- plugins/modules/swim_intent.py | 16 ++++++++-------- plugins/modules/swim_workflow_manager.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index fad4f8d079..96465df307 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -741,18 +741,18 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ device_uuid_list = [] if not site_name: - self.log("Site name is not given so cannot fetch the devices associated to site", "INFO") + self.log("Failed to retrieve devices associated with the site due to missing site name", "INFO") return device_uuid_list (site_exists, site_id) = self.site_exists(site_name) if not site_exists: - self.log("""Given site '{0}' is not present in Cisco Catalyst Center so cannot fetch the devices associated - to site""".format(site_name), "INFO") + self.log("""Site '{0}' is not found in the Cisco Catalyst Center, hence unable to fetch associated + devices.""".format(site_name), "INFO") return device_uuid_list if device_series_name: if device_series_name.startswith(".*") and device_series_name.endswith(".*"): - self.log("Device series name '{0}' already present in the regex format".format(device_series_name), "INFO") + self.log("Device series name '{0}' is already in the regex format".format(device_series_name), "INFO") else: device_series_name = ".*" + device_series_name + ".*" @@ -773,12 +773,12 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ if len(response) > 0: for item in response: if item["reachabilityStatus"] != "Reachable": - self.log("""Reachability status of device '{0}' is '{1}' so cannot add it for distribution/activation - task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + self.log("""Reachability status of device '{0}' is '{1}', so cannot add it for distribution/activation + task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") continue if "role" in item and (device_role is None or item["role"] == device_role.upper() or device_role.upper() == "ALL"): - self.log("""Successfully fetch the device '{0}' associated to the given site '{1}' for swim distribution/activation - task""".format(item["managementIpAddress"], site_name)) + self.log("""Successfully fetched the device '{0}' associated with the given site '{1}' for SWIM distribution/activation + task.""".format(item["managementIpAddress"], site_name)) device_uuid_list.append(item["instanceUuid"]) return device_uuid_list diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 7a59026c24..495b0e4873 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -727,18 +727,18 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ device_uuid_list = [] if not site_name: - self.log("Site name is not given so cannot fetch the devices associated to site", "INFO") + self.log("Failed to retrieve devices associated with the site due to missing site name", "INFO") return device_uuid_list (site_exists, site_id) = self.site_exists(site_name) if not site_exists: - self.log("""Given site '{0}' is not present in Cisco Catalyst Center so cannot fetch the devices associated - to site""".format(site_name), "INFO") + self.log("""Site '{0}' is not found in the Cisco Catalyst Center, hence unable to fetch associated + devices.""".format(site_name), "INFO") return device_uuid_list if device_series_name: if device_series_name.startswith(".*") and device_series_name.endswith(".*"): - self.log("Device series name '{0}' already present in the regex format".format(device_series_name), "INFO") + self.log("Device series name '{0}' is already in the regex format".format(device_series_name), "INFO") else: device_series_name = ".*" + device_series_name + ".*" @@ -759,12 +759,12 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ if len(response) > 0: for item in response: if item["reachabilityStatus"] != "Reachable": - self.log("""Reachability status of device '{0}' is '{1}' so cannot add it for distribution/activation - task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + self.log("""Reachability status of device '{0}' is '{1}', so cannot add it for distribution/activation + task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") continue if "role" in item and (device_role is None or item["role"] == device_role.upper() or device_role.upper() == "ALL"): - self.log("""Successfully fetch the device '{0}' associated to the given site '{1}' for swim distribution/activation - task""".format(item["managementIpAddress"], site_name)) + self.log("""Successfully fetched the device '{0}' associated with the given site '{1}' for SWIM distribution/activation + task.""".format(item["managementIpAddress"], site_name)) device_uuid_list.append(item["instanceUuid"]) return device_uuid_list From e4c8f120d72af6026a28116e9dfccd47702c6537 Mon Sep 17 00:00:00 2001 From: Abinash Date: Mon, 4 Mar 2024 10:34:59 +0000 Subject: [PATCH 25/44] Fixing Invalid SWIM issue while claiming --- plugins/modules/pnp_intent.py | 10 +++++----- plugins/modules/pnp_workflow_manager.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 36514657aa..7394abea0e 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -474,7 +474,7 @@ def get_claim_params(self): imageinfo = { 'imageId': self.have.get('image_id') } - + configinfo = { 'configId': self.have.get('template_id'), 'configParameters': [ @@ -485,7 +485,7 @@ def get_claim_params(self): ] } - if configinfo["configId"] and self.validated_config[0]["template_params"]: + if configinfo.get("configId") and self.validated_config[0].get("template_params"): if isinstance(self.validated_config[0]["template_params"], dict): if len(self.validated_config[0]["template_params"]) > 0: configinfo["configParameters"] = [] @@ -609,7 +609,7 @@ def get_have(self): 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.log("Device 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 @@ -893,6 +893,7 @@ class instance for further use. params=self.want.get("pnp_params")[0], op_modifies=True, ) + self.get_have().check_return_status() 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() @@ -905,8 +906,7 @@ class instance for further use. ) 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"]: + 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 diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 1d99876e1f..53eb85de93 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -485,7 +485,7 @@ def get_claim_params(self): ] } - if configinfo["configId"] and self.validated_config[0]["template_params"]: + if configinfo.get("configId") and self.validated_config[0].get("template_params"): if isinstance(self.validated_config[0]["template_params"], dict): if len(self.validated_config[0]["template_params"]) > 0: configinfo["configParameters"] = [] @@ -609,7 +609,7 @@ def get_have(self): 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.log("Device 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 @@ -893,6 +893,7 @@ class instance for further use. params=self.want.get("pnp_params")[0], op_modifies=True, ) + self.get_have().check_return_status() 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() @@ -905,8 +906,7 @@ class instance for further use. ) 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"]: + 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 From 1ec9a27a54397ac0c5276a1e987d2858b0cf3cf9 Mon Sep 17 00:00:00 2001 From: Abinash Date: Mon, 4 Mar 2024 10:56:05 +0000 Subject: [PATCH 26/44] Fixing Invalid SWIM issue while claiming --- plugins/modules/pnp_intent.py | 1 - plugins/modules/pnp_workflow_manager.py | 1 - 2 files changed, 2 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 7394abea0e..d3aa68239a 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -474,7 +474,6 @@ def get_claim_params(self): imageinfo = { 'imageId': self.have.get('image_id') } - configinfo = { 'configId': self.have.get('template_id'), 'configParameters': [ diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 53eb85de93..348ca755e2 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -474,7 +474,6 @@ def get_claim_params(self): imageinfo = { 'imageId': self.have.get('image_id') } - configinfo = { 'configId': self.have.get('template_id'), 'configParameters': [ From fd3489ccdcb7c41d991a3c52793a319d16562d5b Mon Sep 17 00:00:00 2001 From: Abinash Date: Mon, 4 Mar 2024 12:32:49 +0000 Subject: [PATCH 27/44] Fixing Invalid SWIM issue while claiming --- plugins/modules/pnp_intent.py | 9 +++++---- plugins/modules/pnp_workflow_manager.py | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index d3aa68239a..66a6e37dde 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -474,6 +474,7 @@ def get_claim_params(self): imageinfo = { 'imageId': self.have.get('image_id') } + template_params = self.validated_config[0].get("template_params") configinfo = { 'configId': self.have.get('template_id'), 'configParameters': [ @@ -484,11 +485,11 @@ def get_claim_params(self): ] } - if configinfo.get("configId") and self.validated_config[0].get("template_params"): - if isinstance(self.validated_config[0]["template_params"], dict): - if len(self.validated_config[0]["template_params"]) > 0: + if configinfo.get("configId") and template_params: + if isinstance(template_params, dict): + if len(template_params) > 0: configinfo["configParameters"] = [] - for key, value in self.validated_config[0]["template_params"].items(): + for key, value in template_params.items(): config_dict = { 'key': key, 'value': value diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 348ca755e2..2585f60391 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -474,6 +474,7 @@ def get_claim_params(self): imageinfo = { 'imageId': self.have.get('image_id') } + template_params = self.validated_config[0].get("template_params") configinfo = { 'configId': self.have.get('template_id'), 'configParameters': [ @@ -484,11 +485,11 @@ def get_claim_params(self): ] } - if configinfo.get("configId") and self.validated_config[0].get("template_params"): - if isinstance(self.validated_config[0]["template_params"], dict): - if len(self.validated_config[0]["template_params"]) > 0: + if configinfo.get("configId") and template_params: + if isinstance(template_params, dict): + if len(template_params) > 0: configinfo["configParameters"] = [] - for key, value in self.validated_config[0]["template_params"].items(): + for key, value in template_params.items(): config_dict = { 'key': key, 'value': value From a88da6f0595e2b3892724e2cd9abd2d4721fe855 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 5 Mar 2024 09:50:14 +0530 Subject: [PATCH 28/44] Fixed the language enum check for the name instead of language --- plugins/modules/template_intent.py | 22 ++++++++++---------- plugins/modules/template_workflow_manager.py | 22 ++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index f4f40c667c..338c34ba5a 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1775,24 +1775,24 @@ def get_containing_templates(self, containing_templates): 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) + + language = item.get("language") + language_list = ["JINJA", "VELOCITY"] + 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() + if language not in language_list: + self.msg = "language under containing templates should be in " + str(language_list) self.status = "failed" return self.check_return_status() diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index e5a6428d47..0a69f59bf2 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -1775,24 +1775,24 @@ def get_containing_templates(self, containing_templates): 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) + + language = item.get("language") + language_list = ["JINJA", "VELOCITY"] + 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() + if language not in language_list: + self.msg = "language under containing templates should be in " + str(language_list) self.status = "failed" return self.check_return_status() From 6f50fea8c78b9b45a635c67794dc0d72865a7558 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 5 Mar 2024 11:39:09 +0530 Subject: [PATCH 29/44] Addressed the PR comments --- plugins/modules/template_intent.py | 13 +++++++------ plugins/modules/template_workflow_manager.py | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 338c34ba5a..3c13027d58 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1776,26 +1776,27 @@ def get_containing_templates(self, containing_templates): containingTemplates[i].update({"id": id}) name = item.get("name") - if name is not None: - containingTemplates[i].update({"name": name}) - else: + if name is None: self.msg = "name is mandatory under containing templates" self.status = "failed" return self.check_return_status() + containingTemplates[i].update({"name": name}) + language = item.get("language") language_list = ["JINJA", "VELOCITY"] - if language is not None: - containingTemplates[i].update({"language": language}) - else: + if language is None: self.msg = "language is mandatory under containing templates" self.status = "failed" return self.check_return_status() + if language not in language_list: self.msg = "language under containing templates should be in " + str(language_list) self.status = "failed" return self.check_return_status() + containingTemplates[i].update({"language": language}) + project_name = item.get("project_name") if project_name is not None: containingTemplates[i].update({"projectName": project_name}) diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index 0a69f59bf2..c760966d06 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -1776,26 +1776,27 @@ def get_containing_templates(self, containing_templates): containingTemplates[i].update({"id": id}) name = item.get("name") - if name is not None: - containingTemplates[i].update({"name": name}) - else: + if name is None: self.msg = "name is mandatory under containing templates" self.status = "failed" return self.check_return_status() + containingTemplates[i].update({"name": name}) + language = item.get("language") language_list = ["JINJA", "VELOCITY"] - if language is not None: - containingTemplates[i].update({"language": language}) - else: + if language is None: self.msg = "language is mandatory under containing templates" self.status = "failed" return self.check_return_status() + if language not in language_list: self.msg = "language under containing templates should be in " + str(language_list) self.status = "failed" return self.check_return_status() + containingTemplates[i].update({"language": language}) + project_name = item.get("project_name") if project_name is not None: containingTemplates[i].update({"projectName": project_name}) From e4b11530a5bbdc39eeb090384dae495d12658df2 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 5 Mar 2024 12:29:26 +0530 Subject: [PATCH 30/44] Addressed the review comments --- plugins/modules/template_intent.py | 2 +- plugins/modules/template_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 3c13027d58..e3d74b3568 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1784,12 +1784,12 @@ def get_containing_templates(self, containing_templates): containingTemplates[i].update({"name": name}) language = item.get("language") - language_list = ["JINJA", "VELOCITY"] if language is None: self.msg = "language is mandatory under containing templates" self.status = "failed" return self.check_return_status() + language_list = ["JINJA", "VELOCITY"] if language not in language_list: self.msg = "language under containing templates should be in " + str(language_list) self.status = "failed" diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index c760966d06..909c4c9f81 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -1784,12 +1784,12 @@ def get_containing_templates(self, containing_templates): containingTemplates[i].update({"name": name}) language = item.get("language") - language_list = ["JINJA", "VELOCITY"] if language is None: self.msg = "language is mandatory under containing templates" self.status = "failed" return self.check_return_status() + language_list = ["JINJA", "VELOCITY"] if language not in language_list: self.msg = "language under containing templates should be in " + str(language_list) self.status = "failed" From 18033d2140d4f1be26f2fd695e5c9be1096af1b3 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 5 Mar 2024 14:49:04 +0530 Subject: [PATCH 31/44] =?UTF-8?q?Handle=20the=20case=20if=20site=20name=20?= =?UTF-8?q?is=20not=20given=20then=20will=20site=20name=20as=20=E2=80=9CGl?= =?UTF-8?q?obal=E2=80=9D=20and=20fetch=20all=20devices=20under=20Global=20?= =?UTF-8?q?for=20getting=20device=5Fuuid=5Flist=20for=20swim=20distribute/?= =?UTF-8?q?activate=20operation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/modules/swim_intent.py | 70 +++++++++++++++++------ plugins/modules/swim_workflow_manager.py | 72 ++++++++++++++++++------ 2 files changed, 110 insertions(+), 32 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 96465df307..43c15ff646 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -497,6 +497,7 @@ ) from ansible.module_utils.basic import AnsibleModule import os +import json class DnacSwims(DnacBase): @@ -741,8 +742,8 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ device_uuid_list = [] if not site_name: - self.log("Failed to retrieve devices associated with the site due to missing site name", "INFO") - return device_uuid_list + site_name = "Global" + self.log("Since site name is not given so it will be fetch all the devices under Global and mark site name as 'Global'", "INFO") (site_exists, site_id) = self.site_exists(site_name) if not site_exists: @@ -758,8 +759,7 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ site_params = { "site_id": site_id, - "device_family": device_family, - "device_series_name": device_series_name + "device_family": device_family } response = self.dnac._exec( family="sites", @@ -768,18 +768,56 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ 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": - self.log("""Reachability status of device '{0}' is '{1}', so cannot add it for distribution/activation - task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") - continue - if "role" in item and (device_role is None or item["role"] == device_role.upper() or device_role.upper() == "ALL"): - self.log("""Successfully fetched the device '{0}' associated with the given site '{1}' for SWIM distribution/activation - task.""".format(item["managementIpAddress"], site_name)) - device_uuid_list.append(item["instanceUuid"]) + response = response['device'] + + site_response_list = [] + for item in response: + if item['response']: + for item_dict in item['response']: + site_response_list.append(item_dict) + + if device_role.upper() == 'ALL': + device_role = None + + device_params = { + 'series': device_series_name, + 'family': device_family, + 'role': device_role + } + device_list_response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=True, + params=device_params, + ) + + device_response = device_list_response.get('response') + if not response or not device_response: + self.log("Failed to retrieve devices associated with the site due to empty API response.") + return device_uuid_list + + site_memberships_ids, device_response_ids = [], [] + + for item in site_response_list: + if item["reachabilityStatus"] != "Reachable": + self.log("""Reachability status of device '{0}' is '{1}', so cannot add it for distribution/activation + task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + continue + self.log("""Successfully fetched the device '{0}' associated with the given site '{1}' for SWIM distribution/activation + task.""".format(item["managementIpAddress"], site_name)) + site_memberships_ids.append(item["instanceUuid"]) + + for item in device_response: + if item["reachabilityStatus"] != "Reachable": + self.log("""Reachability status of device '{0}' is '{1}', so cannot add it for distribution/activation + task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + continue + self.log("""Successfully fetched the device '{0}' with given device filter for SWIM distribution/activation + task.""".format(item["managementIpAddress"])) + device_response_ids.append(item["instanceUuid"]) + + # Find the intersection of device IDs with the response get from get_membership api and get_device_list api with provided filters + device_uuid_list = set(site_memberships_ids).intersection(set(device_response_ids)) return device_uuid_list diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 495b0e4873..c05cdf4a05 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -484,6 +484,7 @@ ) from ansible.module_utils.basic import AnsibleModule import os +import json class Swim(DnacBase): @@ -727,8 +728,8 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ device_uuid_list = [] if not site_name: - self.log("Failed to retrieve devices associated with the site due to missing site name", "INFO") - return device_uuid_list + site_name = "Global" + self.log("Since site name is not given so it will be fetch all the devices under Global and mark site name as 'Global'", "INFO") (site_exists, site_id) = self.site_exists(site_name) if not site_exists: @@ -744,8 +745,7 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ site_params = { "site_id": site_id, - "device_family": device_family, - "device_series_name": device_series_name + "device_family": device_family } response = self.dnac._exec( family="sites", @@ -754,18 +754,58 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ 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": - self.log("""Reachability status of device '{0}' is '{1}', so cannot add it for distribution/activation - task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") - continue - if "role" in item and (device_role is None or item["role"] == device_role.upper() or device_role.upper() == "ALL"): - self.log("""Successfully fetched the device '{0}' associated with the given site '{1}' for SWIM distribution/activation - task.""".format(item["managementIpAddress"], site_name)) - device_uuid_list.append(item["instanceUuid"]) + response = response['device'] + + site_response_list = [] + for item in response: + if item['response']: + for item_dict in item['response']: + site_response_list.append(item_dict) + + if device_role.upper() == 'ALL': + device_role = None + + device_params = { + 'series': device_series_name, + 'family': device_family, + 'role': device_role + } + device_list_response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=True, + params=device_params, + ) + + device_response = device_list_response.get('response') + if not response or not device_response: + self.log("Failed to retrieve devices associated with the site due to empty API response.") + return device_uuid_list + + site_memberships_ids, device_response_ids = [], [] + + for item in site_response_list: + # import epdb; + # epdb.serve(port=8888) + if item["reachabilityStatus"] != "Reachable": + self.log("""Reachability status of device '{0}' is '{1}' with site membership api, so cannot add it for distribution/activation + task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + continue + self.log("""Successfully fetched the device '{0}' associated with the given site '{1}' for SWIM distribution/activation + task.""".format(item["managementIpAddress"], site_name)) + site_memberships_ids.append(item["instanceUuid"]) + + for item in device_response: + if item["reachabilityStatus"] != "Reachable": + self.log("""Reachability status of device '{0}' is '{1}' with device list api, so cannot add it for distribution/activation + task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + continue + self.log("""Successfully fetched the device '{0}' with given device filter for SWIM distribution/activation + task.""".format(item["managementIpAddress"])) + device_response_ids.append(item["instanceUuid"]) + + # Find the intersection of device IDs with the response get from get_membership api and get_device_list api with provided filters + device_uuid_list = set(site_memberships_ids).intersection(set(device_response_ids)) return device_uuid_list From 3f4c89a9a351a5b636b9e997fad8ac9062623af6 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 5 Mar 2024 14:56:54 +0530 Subject: [PATCH 32/44] remove unused import json and debugger --- plugins/modules/swim_intent.py | 1 - plugins/modules/swim_workflow_manager.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 43c15ff646..6a6ce1e510 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -497,7 +497,6 @@ ) from ansible.module_utils.basic import AnsibleModule import os -import json class DnacSwims(DnacBase): diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index c05cdf4a05..26821508af 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -484,7 +484,6 @@ ) from ansible.module_utils.basic import AnsibleModule import os -import json class Swim(DnacBase): @@ -785,8 +784,6 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ site_memberships_ids, device_response_ids = [], [] for item in site_response_list: - # import epdb; - # epdb.serve(port=8888) if item["reachabilityStatus"] != "Reachable": self.log("""Reachability status of device '{0}' is '{1}' with site membership api, so cannot add it for distribution/activation task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") From 56a6bf6708524fbe87f43408b6bd929545dd7f7d Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 5 Mar 2024 09:28:21 +0000 Subject: [PATCH 33/44] Adding the feature of non-mandatory global credentials --- playbooks/discovery_intent.yml | 39 ++++- playbooks/discovery_workflow_manager.yml | 39 ++++- plugins/modules/discovery_intent.py | 160 +++++++++++++----- plugins/modules/discovery_workflow_manager.py | 160 +++++++++++++----- 4 files changed, 310 insertions(+), 88 deletions(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index 4837b49efd..db503456bf 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -53,7 +53,7 @@ username: snmpV3 protocol_order: ssh - - name: Execute discovery of single device using various discovery specific credentials + - name: Execute discovery of single device using various discovery specific credentials and all the global credentials cisco.dnac.discovery_intent: <<: *dnac_login state: merged @@ -89,6 +89,43 @@ global_cli_len: 3 protocol_order: ssh + - name: Execute discovery of single device using various discovery specific credentials only + cisco.dnac.discovery_intent: + <<: *dnac_login + state: merged + config_verify: True + config: + - discovery_name: Single without Global Credentials + discovery_type: "SINGLE" + ip_address_list: + - 204.1.2.5 + discovery_specific_credentials: + cli_credentials_list: + - username: cisco + password: Cisco#123 + enable_password: Cisco#123 + http_read_credential: + username: string + password: Lablab#123 + port: 443 + secure: True + snmp_v2_read_credential: + desc: string + community: string + snmp_v2_write_credential: + desc: string + community: string + snmp_v3_credential: + username: v3Public2 + snmp_mode: AUTHPRIV + auth_type: SHA + auth_password: Lablab#1234 + privacy_type: AES256 + privacy_password: Lablab#1234 + global_cli_len: 3 + use_global_cred: False + protocol_order: ssh + - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials cisco.dnac.discovery_intent: <<: *dnac_login diff --git a/playbooks/discovery_workflow_manager.yml b/playbooks/discovery_workflow_manager.yml index 32b3590f05..187e9974f5 100644 --- a/playbooks/discovery_workflow_manager.yml +++ b/playbooks/discovery_workflow_manager.yml @@ -53,7 +53,7 @@ username: snmpV3 protocol_order: ssh - - name: Execute discovery of single device using various discovery specific credentials + - name: Execute discovery of single device using various discovery specific credentials and all the global credentials cisco.dnac.discovery_workflow_manager: <<: *dnac_login state: merged @@ -89,6 +89,43 @@ global_cli_len: 3 protocol_order: ssh + - name: Execute discovery of single device using various discovery specific credentials only + cisco.dnac.discovery_workflow_manager: + <<: *dnac_login + state: merged + config_verify: True + config: + - discovery_name: Single without Global Credentials + discovery_type: "SINGLE" + ip_address_list: + - 204.1.2.5 + discovery_specific_credentials: + cli_credentials_list: + - username: cisco + password: Cisco#123 + enable_password: Cisco#123 + http_read_credential: + username: string + password: Lablab#123 + port: 443 + secure: True + snmp_v2_read_credential: + desc: string + community: string + snmp_v2_write_credential: + desc: string + community: string + snmp_v3_credential: + username: v3Public2 + snmp_mode: AUTHPRIV + auth_type: SHA + auth_password: Lablab#1234 + privacy_type: AES256 + privacy_password: Lablab#1234 + global_cli_len: 3 + use_global_cred: False + protocol_order: ssh + - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials cisco.dnac.discovery_workflow_manager: <<: *dnac_login diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index ee7667ec20..703b2e5958 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -204,6 +204,12 @@ - Requires valid SSH credentials to work. - Avoid standard ports like 22, 80, and 8080. type: str + use_global_cred: + description: + - Boolean value to determine whether the user wants to use the global credentials by default while performing discovery. + - If set False, global credentials will not be used to discover the devices. + type: bool + default: True global_credentials: description: - Set of various credential types, including CLI, SNMP, HTTP, and NETCONF, that a user has pre-configured in @@ -351,7 +357,7 @@ """ EXAMPLES = r""" -- name: Execute discovery devices +- name: Execute discovery devices with both global credentials and discovery specific credentials cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -428,6 +434,63 @@ retry: integer timeout: integer +- name: Execute discovery devices with discovery specific credentials only + cisco.dnac.discovery_intent: + 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: + - discovery_name: string + discovery_type: string + ip_address_list: list + ip_filter_list: list + cdp_level: string + lldp_level: string + prefered_mgmt_ip_method: string + 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 + net_conf_port: string + use_global_cred: False + start_index: integer + records_to_return: integer + protocol_order: string + retry: integer + timeout: integer + - name: Delete disovery by name cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" @@ -556,7 +619,9 @@ def validate_input(self, state=None): 'retry': {'type': 'int', 'required': False}, 'timeout': {'type': 'str', 'required': False}, 'global_credentials': {'type': 'dict', 'required': False}, - 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'} + 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'}, + 'use_global_cred': {'type': 'bool', 'required': False, + 'default': True} } if state == "merged": @@ -610,12 +675,12 @@ def handle_global_credentials(self, response=None): - response: The response collected from the get_all_global_credentials_v2 API Returns: - - global_credentails_all : The dictionary containing list of IDs of various types of + - global_credentials_all : The dictionary containing list of IDs of various types of Global credentials. """ global_credentials = self.validated_config[0].get("global_credentials") - global_credentails_all = {} + global_credentials_all = {} cli_credentials_list = global_credentials.get('cli_credentials_list') if cli_credentials_list: @@ -623,7 +688,7 @@ def handle_global_credentials(self, response=None): msg = "Global CLI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: - global_credentails_all["cliCredential"] = [] + global_credentials_all["cliCredential"] = [] cred_len = len(cli_credentials_list) if cred_len > 5: cred_len = 5 @@ -631,8 +696,8 @@ def handle_global_credentials(self, response=None): if cli_cred.get('description') and cli_cred.get('username'): for cli in response.get("cliCredential"): if cli.get("description") == cli_cred.get('description') and cli.get("username") == cli_cred.get('username'): - global_credentails_all["cliCredential"].append(cli.get("id")) - global_credentails_all["cliCredential"] = global_credentails_all["cliCredential"][:cred_len] + global_credentials_all["cliCredential"].append(cli.get("id")) + global_credentials_all["cliCredential"] = global_credentials_all["cliCredential"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global CLI credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -643,7 +708,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(http_read_credential_list) > 0: - global_credentails_all["httpsRead"] = [] + global_credentials_all["httpsRead"] = [] cred_len = len(http_read_credential_list) if cred_len > 5: cred_len = 5 @@ -651,8 +716,8 @@ def handle_global_credentials(self, response=None): if http_cred.get('description') and http_cred.get('username'): for http in response.get("httpsRead"): if http.get("description") == http.get('description') and http.get("username") == http.get('username'): - global_credentails_all["httpsRead"].append(http.get("id")) - global_credentails_all["httpsRead"] = global_credentails_all["httpsRead"][:cred_len] + global_credentials_all["httpsRead"].append(http.get("id")) + global_credentials_all["httpsRead"] = global_credentials_all["httpsRead"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global HTTP Read credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -663,7 +728,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(http_write_credential_list) > 0: - global_credentails_all["httpsWrite"] = [] + global_credentials_all["httpsWrite"] = [] cred_len = len(http_write_credential_list) if cred_len > 5: cred_len = 5 @@ -671,8 +736,8 @@ def handle_global_credentials(self, response=None): if http_cred.get('description') and http_cred.get('username'): for http in response.get("httpsWrite"): if http.get("description") == http.get('description') and http.get("username") == http.get('username'): - global_credentails_all["httpsWrite"].append(http.get("id")) - global_credentails_all["httpsWrite"] = global_credentails_all["httpsWrite"][:cred_len] + global_credentials_all["httpsWrite"].append(http.get("id")) + global_credentials_all["httpsWrite"] = global_credentials_all["httpsWrite"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global HTTP Write credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -683,7 +748,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: - global_credentails_all["snmpV2cRead"] = [] + global_credentials_all["snmpV2cRead"] = [] cred_len = len(snmp_v2_read_credential_list) if cred_len > 5: cred_len = 5 @@ -691,8 +756,8 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description'): for snmp in response.get("snmpV2cRead"): if snmp.get("description") == snmp_cred.get('description'): - global_credentails_all["snmpV2cRead"].append(snmp.get("id")) - global_credentails_all["snmpV2cRead"] = global_credentails_all["snmpV2cRead"][:cred_len] + global_credentials_all["snmpV2cRead"].append(snmp.get("id")) + global_credentials_all["snmpV2cRead"] = global_credentials_all["snmpV2cRead"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 Read \ credential to discover the devices" @@ -704,7 +769,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV2 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_write_credential_list) > 0: - global_credentails_all["snmpV2cWrite"] = [] + global_credentials_all["snmpV2cWrite"] = [] cred_len = len(snmp_v2_write_credential_list) if cred_len > 5: cred_len = 5 @@ -712,8 +777,8 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description'): for snmp in response.get("snmpV2cWrite"): if snmp.get("description") == snmp_cred.get('description'): - global_credentails_all["snmpV2cWrite"].append(snmp.get("id")) - global_credentails_all["snmpV2cWrite"] = global_credentails_all["snmpV2cWrite"][:cred_len] + global_credentials_all["snmpV2cWrite"].append(snmp.get("id")) + global_credentials_all["snmpV2cWrite"] = global_credentials_all["snmpV2cWrite"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -724,7 +789,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV3 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v3_credential_list) > 0: - global_credentails_all["snmpV3"] = [] + global_credentials_all["snmpV3"] = [] cred_len = len(snmp_v3_credential_list) if cred_len > 5: cred_len = 5 @@ -732,8 +797,8 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description') and snmp_cred.get('username'): for snmp in response.get("snmpV3"): if snmp.get("description") == snmp_cred.get('description') and snmp.get("username") == snmp_cred.get('username'): - global_credentails_all["snmpV3"].append(snmp.get("id")) - global_credentails_all["snmpV3"] = global_credentails_all["snmpV3"][:cred_len] + global_credentials_all["snmpV3"].append(snmp.get("id")) + global_credentials_all["snmpV3"] = global_credentials_all["snmpV3"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global SNMPV3 \ to discover the devices" @@ -745,7 +810,7 @@ def handle_global_credentials(self, response=None): msg = "Global net Conf Ports be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(net_conf_port_list) > 0: - global_credentails_all["netconfCredential"] = [] + global_credentials_all["netconfCredential"] = [] cred_len = len(net_conf_port_list) if cred_len > 5: cred_len = 5 @@ -753,14 +818,14 @@ def handle_global_credentials(self, response=None): if port.get("description"): for netconf in response.get("netconfCredential"): if port.get('description') == netconf.get('description'): - global_credentails_all["netconfCredential"].append(netconf.get("id")) - global_credentails_all["netconfCredential"] = global_credentails_all["netconfCredential"][:cred_len] + global_credentials_all["netconfCredential"].append(netconf.get("id")) + global_credentials_all["netconfCredential"] = global_credentials_all["netconfCredential"][:cred_len] else: msg = "Please provide description of the Global Netconf port to be used" self.discovery_specific_cred_failure(msg=msg) - self.log("Fetched Global credentials IDs are {0}".format(global_credentails_all), "INFO") - return global_credentails_all + self.log("Fetched Global credentials IDs are {0}".format(global_credentials_all), "INFO") + return global_credentials_all def get_ccc_global_credentials_v2_info(self): """ @@ -783,31 +848,31 @@ 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") - global_credentails_all = {} + global_credentials_all = {} global_credentials = self.validated_config[0].get("global_credentials") if global_credentials: - global_credentails_all = self.handle_global_credentials(response=response) + global_credentials_all = self.handle_global_credentials(response=response) - global_cred_set = set(global_credentails_all.keys()) + global_cred_set = set(global_credentials_all.keys()) response_cred_set = set(response.keys()) diff_keys = response_cred_set.difference(global_cred_set) for key in diff_keys: - global_credentails_all[key] = [] + global_credentials_all[key] = [] if response[key] is None: response[key] = [] total_len = len(response[key]) if total_len > 5: total_len = 5 for element in response.get(key): - global_credentails_all[key].append(element.get('id')) - global_credentails_all[key] = global_credentails_all[key][:total_len] + global_credentials_all[key].append(element.get('id')) + global_credentials_all[key] = global_credentials_all[key][:total_len] - if global_credentails_all == {}: + if global_credentials_all == {}: msg = 'Not found any global credentials to perform discovery' self.log(msg, "WARNING") - return global_credentails_all + return global_credentials_all def get_devices_list_info(self): """ @@ -1059,22 +1124,26 @@ def create_params(self, ip_address_list=None): if self.validated_config[0].get('discovery_specific_credentials'): self.handle_discovery_specific_credentials(new_object_params=new_object_params) - global_credentails_all = self.get_ccc_global_credentials_v2_info() + global_cred_flag = self.validated_config[0].get('use_global_cred') + global_credentials_all = {} + + if global_cred_flag is True: + global_credentials_all = self.get_ccc_global_credentials_v2_info() + for global_cred_list in global_credentials_all.values(): + credential_ids.extend(global_cred_list) + new_object_params['globalCredentialIdList'] = credential_ids + + self.log("All the global credentials used for the discovery task are {0}".format(str(global_credentials_all)), "DEBUG") - self.log(global_credentails_all, "DEBUG") if not (new_object_params.get('snmpUserName') or new_object_params.get('snmpROCommunityDesc') or new_object_params.get('snmpRWCommunityDesc') - or global_credentails_all.get('snmpV2cRead') or global_credentails_all.get('snmpV2cWrite') or global_credentails_all.get('snmpV3')): + or global_credentials_all.get('snmpV2cRead') or global_credentials_all.get('snmpV2cWrite') or global_credentials_all.get('snmpV3')): msg = "Please provide atleast one valid SNMP credential to perform Discovery" self.discovery_specific_cred_failure(msg=msg) - if not (new_object_params.get('userNameList') or global_credentails_all.get('cliCredential')): + if not (new_object_params.get('userNameList') or global_credentials_all.get('cliCredential')): msg = "Please provide atleast one valid CLI credential to perform Discovery" self.discovery_specific_cred_failure(msg=msg) - for global_cred_list in global_credentails_all.values(): - credential_ids.extend(global_cred_list) - - new_object_params['globalCredentialIdList'] = credential_ids self.log("The payload/object created for calling the start discovery API is {0}".format(str(new_object_params)), "INFO") return new_object_params @@ -1279,6 +1348,11 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): self.log("Some devices in the range are reachable", "INFO") break + elif all(res.get('reachabilityStatus') != 'Success' and res.get('inventoryReachabilityStatus') == 'Reachable' for res in devices): + result = True + self.log("Devices are not reachable, but discovery is completed", "WARNING") + break + count += 1 if count == 3: break diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index c61a3d9d9e..163cddb31c 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -204,6 +204,12 @@ - Requires valid SSH credentials to work. - Avoid standard ports like 22, 80, and 8080. type: str + use_global_cred: + description: + - Boolean value to determine whether the user wants to use the global credentials by default while performing discovery. + - If set False, global credentials will not be used to discover the devices. + type: bool + default: True global_credentials: description: - Set of various credential types, including CLI, SNMP, HTTP, and NETCONF, that a user has pre-configured in @@ -351,7 +357,7 @@ """ EXAMPLES = r""" -- name: Execute discovery devices +- name: Execute discovery devices with both global credentials and discovery specific credentials cisco.dnac.discovery_workflow_manager: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -428,6 +434,63 @@ retry: integer timeout: integer +- name: Execute discovery devices with discovery specific credentials only + 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: + - discovery_name: string + discovery_type: string + ip_address_list: list + ip_filter_list: list + cdp_level: string + lldp_level: string + prefered_mgmt_ip_method: string + 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 + net_conf_port: string + use_global_cred: False + start_index: integer + records_to_return: integer + protocol_order: string + retry: integer + timeout: integer + - name: Delete disovery by name cisco.dnac.discovery_workflow_manager: dnac_host: "{{dnac_host}}" @@ -556,7 +619,9 @@ def validate_input(self, state=None): 'retry': {'type': 'int', 'required': False}, 'timeout': {'type': 'str', 'required': False}, 'global_credentials': {'type': 'dict', 'required': False}, - 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'} + 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'}, + 'use_global_cred': {'type': 'bool', 'required': False, + 'default': True} } if state == "merged": @@ -610,12 +675,12 @@ def handle_global_credentials(self, response=None): - response: The response collected from the get_all_global_credentials_v2 API Returns: - - global_credentails_all : The dictionary containing list of IDs of various types of + - global_credentials_all : The dictionary containing list of IDs of various types of Global credentials. """ global_credentials = self.validated_config[0].get("global_credentials") - global_credentails_all = {} + global_credentials_all = {} cli_credentials_list = global_credentials.get('cli_credentials_list') if cli_credentials_list: @@ -623,7 +688,7 @@ def handle_global_credentials(self, response=None): msg = "Global CLI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: - global_credentails_all["cliCredential"] = [] + global_credentials_all["cliCredential"] = [] cred_len = len(cli_credentials_list) if cred_len > 5: cred_len = 5 @@ -631,8 +696,8 @@ def handle_global_credentials(self, response=None): if cli_cred.get('description') and cli_cred.get('username'): for cli in response.get("cliCredential"): if cli.get("description") == cli_cred.get('description') and cli.get("username") == cli_cred.get('username'): - global_credentails_all["cliCredential"].append(cli.get("id")) - global_credentails_all["cliCredential"] = global_credentails_all["cliCredential"][:cred_len] + global_credentials_all["cliCredential"].append(cli.get("id")) + global_credentials_all["cliCredential"] = global_credentials_all["cliCredential"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global CLI credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -643,7 +708,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(http_read_credential_list) > 0: - global_credentails_all["httpsRead"] = [] + global_credentials_all["httpsRead"] = [] cred_len = len(http_read_credential_list) if cred_len > 5: cred_len = 5 @@ -651,8 +716,8 @@ def handle_global_credentials(self, response=None): if http_cred.get('description') and http_cred.get('username'): for http in response.get("httpsRead"): if http.get("description") == http.get('description') and http.get("username") == http.get('username'): - global_credentails_all["httpsRead"].append(http.get("id")) - global_credentails_all["httpsRead"] = global_credentails_all["httpsRead"][:cred_len] + global_credentials_all["httpsRead"].append(http.get("id")) + global_credentials_all["httpsRead"] = global_credentials_all["httpsRead"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global HTTP Read credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -663,7 +728,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(http_write_credential_list) > 0: - global_credentails_all["httpsWrite"] = [] + global_credentials_all["httpsWrite"] = [] cred_len = len(http_write_credential_list) if cred_len > 5: cred_len = 5 @@ -671,8 +736,8 @@ def handle_global_credentials(self, response=None): if http_cred.get('description') and http_cred.get('username'): for http in response.get("httpsWrite"): if http.get("description") == http.get('description') and http.get("username") == http.get('username'): - global_credentails_all["httpsWrite"].append(http.get("id")) - global_credentails_all["httpsWrite"] = global_credentails_all["httpsWrite"][:cred_len] + global_credentials_all["httpsWrite"].append(http.get("id")) + global_credentials_all["httpsWrite"] = global_credentials_all["httpsWrite"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global HTTP Write credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -683,7 +748,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: - global_credentails_all["snmpV2cRead"] = [] + global_credentials_all["snmpV2cRead"] = [] cred_len = len(snmp_v2_read_credential_list) if cred_len > 5: cred_len = 5 @@ -691,8 +756,8 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description'): for snmp in response.get("snmpV2cRead"): if snmp.get("description") == snmp_cred.get('description'): - global_credentails_all["snmpV2cRead"].append(snmp.get("id")) - global_credentails_all["snmpV2cRead"] = global_credentails_all["snmpV2cRead"][:cred_len] + global_credentials_all["snmpV2cRead"].append(snmp.get("id")) + global_credentials_all["snmpV2cRead"] = global_credentials_all["snmpV2cRead"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 Read \ credential to discover the devices" @@ -704,7 +769,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV2 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_write_credential_list) > 0: - global_credentails_all["snmpV2cWrite"] = [] + global_credentials_all["snmpV2cWrite"] = [] cred_len = len(snmp_v2_write_credential_list) if cred_len > 5: cred_len = 5 @@ -712,8 +777,8 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description'): for snmp in response.get("snmpV2cWrite"): if snmp.get("description") == snmp_cred.get('description'): - global_credentails_all["snmpV2cWrite"].append(snmp.get("id")) - global_credentails_all["snmpV2cWrite"] = global_credentails_all["snmpV2cWrite"][:cred_len] + global_credentials_all["snmpV2cWrite"].append(snmp.get("id")) + global_credentials_all["snmpV2cWrite"] = global_credentials_all["snmpV2cWrite"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -724,7 +789,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPV3 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v3_credential_list) > 0: - global_credentails_all["snmpV3"] = [] + global_credentials_all["snmpV3"] = [] cred_len = len(snmp_v3_credential_list) if cred_len > 5: cred_len = 5 @@ -732,8 +797,8 @@ def handle_global_credentials(self, response=None): if snmp_cred.get('description') and snmp_cred.get('username'): for snmp in response.get("snmpV3"): if snmp.get("description") == snmp_cred.get('description') and snmp.get("username") == snmp_cred.get('username'): - global_credentails_all["snmpV3"].append(snmp.get("id")) - global_credentails_all["snmpV3"] = global_credentails_all["snmpV3"][:cred_len] + global_credentials_all["snmpV3"].append(snmp.get("id")) + global_credentials_all["snmpV3"] = global_credentials_all["snmpV3"][:cred_len] else: msg = "Kindly ensure you include both the description and the username for the Global SNMPV3 \ to discover the devices" @@ -745,7 +810,7 @@ def handle_global_credentials(self, response=None): msg = "Global net Conf Ports be passed as a list" self.discovery_specific_cred_failure(msg=msg) if len(net_conf_port_list) > 0: - global_credentails_all["netconfCredential"] = [] + global_credentials_all["netconfCredential"] = [] cred_len = len(net_conf_port_list) if cred_len > 5: cred_len = 5 @@ -753,14 +818,14 @@ def handle_global_credentials(self, response=None): if port.get("description"): for netconf in response.get("netconfCredential"): if port.get('description') == netconf.get('description'): - global_credentails_all["netconfCredential"].append(netconf.get("id")) - global_credentails_all["netconfCredential"] = global_credentails_all["netconfCredential"][:cred_len] + global_credentials_all["netconfCredential"].append(netconf.get("id")) + global_credentials_all["netconfCredential"] = global_credentials_all["netconfCredential"][:cred_len] else: msg = "Please provide description of the Global Netconf port to be used" self.discovery_specific_cred_failure(msg=msg) - self.log("Fetched Global credentials IDs are {0}".format(global_credentails_all), "INFO") - return global_credentails_all + self.log("Fetched Global credentials IDs are {0}".format(global_credentials_all), "INFO") + return global_credentials_all def get_ccc_global_credentials_v2_info(self): """ @@ -783,31 +848,31 @@ 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") - global_credentails_all = {} + global_credentials_all = {} global_credentials = self.validated_config[0].get("global_credentials") if global_credentials: - global_credentails_all = self.handle_global_credentials(response=response) + global_credentials_all = self.handle_global_credentials(response=response) - global_cred_set = set(global_credentails_all.keys()) + global_cred_set = set(global_credentials_all.keys()) response_cred_set = set(response.keys()) diff_keys = response_cred_set.difference(global_cred_set) for key in diff_keys: - global_credentails_all[key] = [] + global_credentials_all[key] = [] if response[key] is None: response[key] = [] total_len = len(response[key]) if total_len > 5: total_len = 5 for element in response.get(key): - global_credentails_all[key].append(element.get('id')) - global_credentails_all[key] = global_credentails_all[key][:total_len] + global_credentials_all[key].append(element.get('id')) + global_credentials_all[key] = global_credentials_all[key][:total_len] - if global_credentails_all == {}: + if global_credentials_all == {}: msg = 'Not found any global credentials to perform discovery' self.log(msg, "WARNING") - return global_credentails_all + return global_credentials_all def get_devices_list_info(self): """ @@ -1059,22 +1124,26 @@ def create_params(self, ip_address_list=None): if self.validated_config[0].get('discovery_specific_credentials'): self.handle_discovery_specific_credentials(new_object_params=new_object_params) - global_credentails_all = self.get_ccc_global_credentials_v2_info() + global_cred_flag = self.validated_config[0].get('use_global_cred') + global_credentials_all = {} + + if global_cred_flag is True: + global_credentials_all = self.get_ccc_global_credentials_v2_info() + for global_cred_list in global_credentials_all.values(): + credential_ids.extend(global_cred_list) + new_object_params['globalCredentialIdList'] = credential_ids + + self.log("All the global credentials used for the discovery task are {0}".format(str(global_credentials_all)), "DEBUG") - self.log(global_credentails_all, "DEBUG") if not (new_object_params.get('snmpUserName') or new_object_params.get('snmpROCommunityDesc') or new_object_params.get('snmpRWCommunityDesc') - or global_credentails_all.get('snmpV2cRead') or global_credentails_all.get('snmpV2cWrite') or global_credentails_all.get('snmpV3')): + or global_credentials_all.get('snmpV2cRead') or global_credentials_all.get('snmpV2cWrite') or global_credentials_all.get('snmpV3')): msg = "Please provide atleast one valid SNMP credential to perform Discovery" self.discovery_specific_cred_failure(msg=msg) - if not (new_object_params.get('userNameList') or global_credentails_all.get('cliCredential')): + if not (new_object_params.get('userNameList') or global_credentials_all.get('cliCredential')): msg = "Please provide atleast one valid CLI credential to perform Discovery" self.discovery_specific_cred_failure(msg=msg) - for global_cred_list in global_credentails_all.values(): - credential_ids.extend(global_cred_list) - - new_object_params['globalCredentialIdList'] = credential_ids self.log("The payload/object created for calling the start discovery API is {0}".format(str(new_object_params)), "INFO") return new_object_params @@ -1279,6 +1348,11 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): self.log("Some devices in the range are reachable", "INFO") break + elif all(res.get('reachabilityStatus') != 'Success' and res.get('inventoryReachabilityStatus') == 'Reachable' for res in devices): + result = True + self.log("Devices are not reachable, but discovery is completed", "WARNING") + break + count += 1 if count == 3: break From 22515b8ccdcac65841eb82ff80b1af4af7f0f055 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 5 Mar 2024 10:06:32 +0000 Subject: [PATCH 34/44] Adding the feature of non-mandatory global credentials --- plugins/modules/discovery_intent.py | 3 ++- plugins/modules/discovery_workflow_manager.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 703b2e5958..a5e9df52dd 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -207,7 +207,8 @@ use_global_cred: description: - Boolean value to determine whether the user wants to use the global credentials by default while performing discovery. - - If set False, global credentials will not be used to discover the devices. + - If set to False, global credentials will not be used to discover the devices, and at least one discovery-specific SNMP + and CLI credential is required. type: bool default: True global_credentials: diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 163cddb31c..c2ff7829a0 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -207,7 +207,8 @@ use_global_cred: description: - Boolean value to determine whether the user wants to use the global credentials by default while performing discovery. - - If set False, global credentials will not be used to discover the devices. + - If set to False, global credentials will not be used to discover the devices, and at least one discovery-specific + SNMP and CLI credential is required. type: bool default: True global_credentials: From 781c5141d8678914ec02f6ebe836092b01f531ff Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 5 Mar 2024 15:55:47 +0530 Subject: [PATCH 35/44] updated log messages for swim --- plugins/modules/swim_intent.py | 18 +++++++++--------- plugins/modules/swim_workflow_manager.py | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 6a6ce1e510..9d6425653b 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -792,27 +792,27 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ device_response = device_list_response.get('response') if not response or not device_response: - self.log("Failed to retrieve devices associated with the site due to empty API response.") + self.log("Failed to retrieve devices associated with the site '{0}' due to empty API response.".format(site_name), "INFO") return device_uuid_list site_memberships_ids, device_response_ids = [], [] for item in site_response_list: if item["reachabilityStatus"] != "Reachable": - self.log("""Reachability status of device '{0}' is '{1}', so cannot add it for distribution/activation - task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + self.log("""Device '{0}' is currently '{1}' and cannot be included in the SWIM distribution/activation + process.""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") continue - self.log("""Successfully fetched the device '{0}' associated with the given site '{1}' for SWIM distribution/activation - task.""".format(item["managementIpAddress"], site_name)) + self.log("""Device '{0}' from site '{1}' is ready for the SWIM distribution/activation + process.""".format(item["managementIpAddress"], site_name), "INFO") site_memberships_ids.append(item["instanceUuid"]) for item in device_response: if item["reachabilityStatus"] != "Reachable": - self.log("""Reachability status of device '{0}' is '{1}', so cannot add it for distribution/activation - task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + self.log("""Unable to proceed with the device '{0}' for SWIM distribution/activation as its status is + '{1}'.""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") continue - self.log("""Successfully fetched the device '{0}' with given device filter for SWIM distribution/activation - task.""".format(item["managementIpAddress"])) + self.log("""Device '{0}' matches to the specified filter requirements and is set for SWIM + distribution/activation.""".format(item["managementIpAddress"]), "INFO") device_response_ids.append(item["instanceUuid"]) # Find the intersection of device IDs with the response get from get_membership api and get_device_list api with provided filters diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 26821508af..ff364296e6 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -728,7 +728,7 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ device_uuid_list = [] if not site_name: site_name = "Global" - self.log("Since site name is not given so it will be fetch all the devices under Global and mark site name as 'Global'", "INFO") + self.log("Site name not specified; defaulting to 'Global' to fetch all devices under this category", "INFO") (site_exists, site_id) = self.site_exists(site_name) if not site_exists: @@ -778,27 +778,27 @@ def get_device_uuids(self, site_name, device_family, device_role, device_series_ device_response = device_list_response.get('response') if not response or not device_response: - self.log("Failed to retrieve devices associated with the site due to empty API response.") + self.log("Failed to retrieve devices associated with the site '{0}' due to empty API response.".format(site_name), "INFO") return device_uuid_list site_memberships_ids, device_response_ids = [], [] for item in site_response_list: if item["reachabilityStatus"] != "Reachable": - self.log("""Reachability status of device '{0}' is '{1}' with site membership api, so cannot add it for distribution/activation - task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + self.log("""Device '{0}' is currently '{1}' and cannot be included in the SWIM distribution/activation + process.""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") continue - self.log("""Successfully fetched the device '{0}' associated with the given site '{1}' for SWIM distribution/activation - task.""".format(item["managementIpAddress"], site_name)) + self.log("""Device '{0}' from site '{1}' is ready for the SWIM distribution/activation + process.""".format(item["managementIpAddress"], site_name), "INFO") site_memberships_ids.append(item["instanceUuid"]) for item in device_response: if item["reachabilityStatus"] != "Reachable": - self.log("""Reachability status of device '{0}' is '{1}' with device list api, so cannot add it for distribution/activation - task of swim image""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") + self.log("""Unable to proceed with the device '{0}' for SWIM distribution/activation as its status is + '{1}'.""".format(item["managementIpAddress"], item["reachabilityStatus"]), "INFO") continue - self.log("""Successfully fetched the device '{0}' with given device filter for SWIM distribution/activation - task.""".format(item["managementIpAddress"])) + self.log("""Device '{0}' matches to the specified filter requirements and is set for SWIM + distribution/activation.""".format(item["managementIpAddress"]), "INFO") device_response_ids.append(item["instanceUuid"]) # Find the intersection of device IDs with the response get from get_membership api and get_device_list api with provided filters From 4da65c4dc3ef1d70a2c411bb1076303963d1fa95 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 5 Mar 2024 14:37:33 +0000 Subject: [PATCH 36/44] Adding the feature of non-mandatory global credentials --- plugins/modules/discovery_intent.py | 20 ++++++++--------- plugins/modules/discovery_workflow_manager.py | 22 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index a5e9df52dd..9e25dc588d 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -79,6 +79,13 @@ description: Preferred method for the management of the IP (None/UseLoopBack) type: str default: None + use_global_credentials: + description: + - Determines if device discovery should utilize pre-configured global credentials. + - Setting to True employs the predefined global credentials for discovery tasks. This is the default setting. + - Setting to False requires manually provided, device-specific credentials for discovery, as global credentials will be bypassed. + type: bool + default: True discovery_specific_credentials: description: Credentials specifically created by the user for performing device discovery. type: dict @@ -204,13 +211,6 @@ - Requires valid SSH credentials to work. - Avoid standard ports like 22, 80, and 8080. type: str - use_global_cred: - description: - - Boolean value to determine whether the user wants to use the global credentials by default while performing discovery. - - If set to False, global credentials will not be used to discover the devices, and at least one discovery-specific SNMP - and CLI credential is required. - type: bool - default: True global_credentials: description: - Set of various credential types, including CLI, SNMP, HTTP, and NETCONF, that a user has pre-configured in @@ -485,7 +485,7 @@ privacy_type: string privacy_password: string net_conf_port: string - use_global_cred: False + use_global_credentials: False start_index: integer records_to_return: integer protocol_order: string @@ -621,7 +621,7 @@ def validate_input(self, state=None): 'timeout': {'type': 'str', 'required': False}, 'global_credentials': {'type': 'dict', 'required': False}, 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'}, - 'use_global_cred': {'type': 'bool', 'required': False, + 'use_global_credentials': {'type': 'bool', 'required': False, 'default': True} } @@ -1125,7 +1125,7 @@ def create_params(self, ip_address_list=None): if self.validated_config[0].get('discovery_specific_credentials'): self.handle_discovery_specific_credentials(new_object_params=new_object_params) - global_cred_flag = self.validated_config[0].get('use_global_cred') + global_cred_flag = self.validated_config[0].get('use_global_credentials') global_credentials_all = {} if global_cred_flag is True: diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index c2ff7829a0..89761704a4 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -79,6 +79,13 @@ description: Preferred method for the management of the IP (None/UseLoopBack) type: str default: None + use_global_credentials: + description: + - Determines if device discovery should utilize pre-configured global credentials. + - Setting to True employs the predefined global credentials for discovery tasks. This is the default setting. + - Setting to False requires manually provided, device-specific credentials for discovery, as global credentials will be bypassed. + type: bool + default: True discovery_specific_credentials: description: Credentials specifically created by the user for performing device discovery. type: dict @@ -204,13 +211,6 @@ - Requires valid SSH credentials to work. - Avoid standard ports like 22, 80, and 8080. type: str - use_global_cred: - description: - - Boolean value to determine whether the user wants to use the global credentials by default while performing discovery. - - If set to False, global credentials will not be used to discover the devices, and at least one discovery-specific - SNMP and CLI credential is required. - type: bool - default: True global_credentials: description: - Set of various credential types, including CLI, SNMP, HTTP, and NETCONF, that a user has pre-configured in @@ -359,7 +359,7 @@ EXAMPLES = r""" - name: Execute discovery devices with both global credentials and discovery specific credentials - cisco.dnac.discovery_workflow_manager: + cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" dnac_password: "{{dnac_password}}" @@ -485,7 +485,7 @@ privacy_type: string privacy_password: string net_conf_port: string - use_global_cred: False + use_global_credentials: False start_index: integer records_to_return: integer protocol_order: string @@ -621,7 +621,7 @@ def validate_input(self, state=None): 'timeout': {'type': 'str', 'required': False}, 'global_credentials': {'type': 'dict', 'required': False}, 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'}, - 'use_global_cred': {'type': 'bool', 'required': False, + 'use_global_credentials': {'type': 'bool', 'required': False, 'default': True} } @@ -1125,7 +1125,7 @@ def create_params(self, ip_address_list=None): if self.validated_config[0].get('discovery_specific_credentials'): self.handle_discovery_specific_credentials(new_object_params=new_object_params) - global_cred_flag = self.validated_config[0].get('use_global_cred') + global_cred_flag = self.validated_config[0].get('use_global_credentials') global_credentials_all = {} if global_cred_flag is True: From 8e2fa39c747346da6d703c1300fb39f102ef9c37 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 5 Mar 2024 14:45:15 +0000 Subject: [PATCH 37/44] Adding the feature of non-mandatory global credentials --- plugins/modules/discovery_intent.py | 3 +-- plugins/modules/discovery_workflow_manager.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 9e25dc588d..0c879cf8b2 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -621,8 +621,7 @@ def validate_input(self, state=None): 'timeout': {'type': 'str', 'required': False}, 'global_credentials': {'type': 'dict', 'required': False}, 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'}, - 'use_global_credentials': {'type': 'bool', 'required': False, - 'default': True} + 'use_global_credentials': {'type': 'bool', 'required': False, 'default': True} } if state == "merged": diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 89761704a4..6d23ef7222 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -621,8 +621,7 @@ def validate_input(self, state=None): 'timeout': {'type': 'str', 'required': False}, 'global_credentials': {'type': 'dict', 'required': False}, 'protocol_order': {'type': 'str', 'required': False, 'default': 'ssh'}, - 'use_global_credentials': {'type': 'bool', 'required': False, - 'default': True} + 'use_global_credentials': {'type': 'bool', 'required': False, 'default': True} } if state == "merged": From 7d5bad6ce7142d6ac9e86a60769e52403070fc02 Mon Sep 17 00:00:00 2001 From: Madhan Date: Tue, 5 Mar 2024 21:22:22 +0530 Subject: [PATCH 38/44] Changing the module name --- plugins/modules/discovery_workflow_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 6d23ef7222..8e4f484330 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -359,7 +359,7 @@ EXAMPLES = r""" - name: Execute discovery devices with both global credentials and discovery specific credentials - cisco.dnac.discovery_intent: + cisco.dnac.discovery_workflow_manager: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" dnac_password: "{{dnac_password}}" From 255b3c23edada46af12483750774c5d3b83fda80 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 6 Mar 2024 23:02:29 +0530 Subject: [PATCH 39/44] Changed the documentation as per the guidelines --- changelogs/changelog.yaml | 18 ++++++++++++++++-- plugins/modules/discovery_intent.py | 3 +++ plugins/modules/discovery_workflow_manager.py | 3 +++ plugins/modules/inventory_intent.py | 11 +++++++++++ plugins/modules/inventory_workflow_manager.py | 11 +++++++++++ plugins/modules/swim_intent.py | 2 ++ plugins/modules/swim_workflow_manager.py | 2 ++ 7 files changed, 48 insertions(+), 2 deletions(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index d1f6b4cac2..909e4cdcff 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -806,10 +806,24 @@ releases: - 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. - 6.11.1: - release_date: "2024-03-02" + 6.12.0: + release_date: "2024-03-06" changes: release_summary: Enhancements in discovery and inventory workflow manager modules. minor_changes: - Changes in discovery workflow manager module to support SNMP credentials v2 and handling error messages. - Changes in inventory workflow manager module to support snmp v2. + - swim_workflow_manager - attribute 'device_series_name' was added. + - swim_intent - attribute 'device_series_name' was added. + - discovery_workflow_manager - attribute 'global_credentials' was added and 'global_cli_len' was removed. + - discovery_intent - attribute 'global_credentials' was added and 'global_cli_len' was removed. + - > + inventory_workflow_manager - attributes 'serial_number', 'device_added', 'role_source' were removed. + attributes 'clear_mac_address_table', 'device_ip', 'resync_retry_count', 'resync_retry_interval', + 'reprovision_wired_device', 'provision_wireless_device' were added. + Renamed argument from 'ip_address' to 'ip_address_list'. + - > + inventory_intent - attributes 'serial_number', 'device_added', 'role_source' were removed. + attributes 'clear_mac_address_table', 'device_ip', 'resync_retry_count', 'resync_retry_interval', + 'reprovision_wired_device', 'provision_wireless_device' were added. + Renamed argument from 'ip_address' to 'ip_address_list'. diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 0c879cf8b2..8da0536165 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -218,6 +218,7 @@ - If user doesn't pass any global credentials in the playbook, then by default, we will use all the global credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) type: dict + version_added: 6.12.0 suboptions: cli_credentials_list: description: @@ -355,6 +356,8 @@ delete /dna/intent/api/v1/delete get /dna/intent/api/v1/discovery/count + - Removed 'global_cli_len' option in v6.12.0. + """ EXAMPLES = r""" diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 8e4f484330..3274653cae 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -218,6 +218,7 @@ - If user doesn't pass any global credentials in the playbook, then by default, we will use all the global credentials present in the Cisco Catalyst Center of each type for performing discovery. (Max 5 allowed) type: dict + version_added: 6.12.0 suboptions: cli_credentials_list: description: @@ -355,6 +356,8 @@ delete /dna/intent/api/v1/delete get /dna/intent/api/v1/discovery/count + - Removed 'global_cli_len' option in v6.12.0. + """ EXAMPLES = r""" diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index f0eea96d51..a5b5f5d55c 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -249,6 +249,7 @@ with a default value of False. type: bool default: False + version_added: 6.12.0 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. @@ -268,6 +269,7 @@ description: Specifies the IP address of the wired device. This is a string value that should be in the format of standard IPv4 or IPv6 addresses. type: str + version_added: 6.12.0 site_name: description: Indicates the exact location where the wired device will be provisioned. This is a string value that should represent the complete hierarchical path of the site (For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). @@ -277,16 +279,19 @@ the provisioning process. If unspecified, the default value is set to 200 retries. type: int default: 200 + version_added: 6.12.0 resync_retry_interval: description: Sets the interval, in seconds, at which the system will recheck the device status throughout the provisioning process. If unspecified, the system will check the device status every 2 seconds by default. type: int default: 2 + version_added: 6.12.0 reprovision_wired_device: description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wired device and the name of the site where the device will be re-provisioned. type: list elements: dict + version_added: 6.12.0 suboptions: device_ip: description: Specifies the IP address of the wired device. This is a string value that should be in the format of @@ -301,6 +306,7 @@ the name of the site where the device will be provisioned along with dynamic interface details. type: list elements: dict + version_added: 6.12.0 suboptions: device_ip: description: Specifies the IP address of the wirelesss device. This is a string value that should be in the format of @@ -375,6 +381,11 @@ put /dna/intent/api/v1/network-device, - Removed 'managementIpAddress' options in v4.3.0. + + - Renamed argument 'ip_address' to 'ip_address_list' option in v6.12.0. + + - Removed 'serial_number', 'device_added', 'role_source', options in v6.12.0. + """ EXAMPLES = r""" diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 01f12dc9b4..9c8e01a24b 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -250,6 +250,7 @@ with a default value of False. type: bool default: False + version_added: 6.12.0 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. @@ -269,6 +270,7 @@ description: Specifies the IP address of the wired device. This is a string value that should be in the format of standard IPv4 or IPv6 addresses. type: str + version_added: 6.12.0 site_name: description: Indicates the exact location where the wired device will be provisioned. This is a string value that should represent the complete hierarchical path of the site (For example, "Global/USA/San Francisco/BGL_18/floor_pnp"). @@ -278,16 +280,19 @@ the provisioning process. If unspecified, the default value is set to 200 retries. type: int default: 200 + version_added: 6.12.0 resync_retry_interval: description: Sets the interval, in seconds, at which the system will recheck the device status throughout the provisioning process. If unspecified, the system will check the device status every 2 seconds by default. type: int default: 2 + version_added: 6.12.0 reprovision_wired_device: description: This parameter takes a list of dictionaries. Each dictionary provides the IP address of a wired device and the name of the site where the device will be re-provisioned. type: list elements: dict + version_added: 6.12.0 suboptions: device_ip: description: Specifies the IP address of the wired device. This is a string value that should be in the format of @@ -302,6 +307,7 @@ the name of the site where the device will be provisioned along with dynamic interface details. type: list elements: dict + version_added: 6.12.0 suboptions: device_ip: description: Specifies the IP address of the wirelesss device. This is a string value that should be in the format of @@ -376,6 +382,11 @@ put /dna/intent/api/v1/network-device, - Removed 'managementIpAddress' options in v4.3.0. + + - Renamed argument 'ip_address' to 'ip_address_list' option in v6.12.0. + + - Removed 'serial_number', 'device_added', 'role_source', options in v6.12.0. + """ EXAMPLES = r""" diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 9d6425653b..a5d8fe61c5 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -185,6 +185,7 @@ description: This parameter specifies the name of the device series. It is used to identify a specific series of devices, such as Cisco Catalyst 9300 Series Switches, within the Cisco Catalyst Center. type: str + version_added: 6.12.0 tagging: description: Booelan value to tag/untag SWIM image as golden If True then the given image will be tagged as golden. @@ -223,6 +224,7 @@ description: This parameter specifies the name of the device series. It is used to identify a specific series of devices, such as Cisco Catalyst 9300 Series Switches, within the Cisco Catalyst Center. type: str + version_added: 6.12.0 image_name: description: SWIM image's name type: str diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index ff364296e6..b40596e5ad 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -219,6 +219,7 @@ description: This parameter specifies the name of the device series. It is used to identify a specific series of devices, such as Cisco Catalyst 9300 Series Switches, within the Cisco Catalyst Center. type: str + version_added: 6.12.0 image_name: description: SWIM image's name type: str @@ -254,6 +255,7 @@ description: This parameter specifies the name of the device series. It is used to identify a specific series of devices, such as Cisco Catalyst 9300 Series Switches, within the Cisco Catalyst Center. type: str + version_added: 6.12.0 activate_lower_image_version: description: ActivateLowerImageVersion flag. type: bool From 81f0044f15fd7ec144f5f568b165862c1bb2153c Mon Sep 17 00:00:00 2001 From: Madhan Date: Wed, 6 Mar 2024 23:12:24 +0530 Subject: [PATCH 40/44] Changing the version --- galaxy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy.yml b/galaxy.yml index 653b3cf22a..69be2d0998 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.11.1 +version: 6.12.0 readme: README.md authors: - Rafael Campos From 831479133a563cf3c60515e55bd5f60fa2ffe1d6 Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 7 Mar 2024 06:35:42 +0000 Subject: [PATCH 41/44] Adding fix for Stackswitch getting changed to normal switch post deiting the device's info --- plugins/modules/pnp_intent.py | 7 +++++++ plugins/modules/pnp_workflow_manager.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 66a6e37dde..a0926d1ec4 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -943,6 +943,9 @@ class instance for further use. ) self.log("Response from 'get_device_by_id' API for device details: {0}".format(str(dev_details_response)), "DEBUG") + is_stack = False + if dev_details_response.get("deviceInfo").get("stack"): + is_stack = dev_details_response.get("deviceInfo").get("stack") pnp_state = dev_details_response.get("deviceInfo").get("state") self.log("PnP state of the device: {0}".format(pnp_state), "INFO") @@ -953,6 +956,10 @@ class instance for further use. return self update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} + if is_stack is True: + update_payload["deviceInfo"]["stack"] = True + + self.log("The request sent for 'update_device' API for device's config update: {0}".format(update_payload), "DEBUG") update_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="update_device", diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 2585f60391..15149953fa 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -943,6 +943,9 @@ class instance for further use. ) self.log("Response from 'get_device_by_id' API for device details: {0}".format(str(dev_details_response)), "DEBUG") + is_stack = False + if dev_details_response.get("deviceInfo").get("stack"): + is_stack = dev_details_response.get("deviceInfo").get("stack") pnp_state = dev_details_response.get("deviceInfo").get("state") self.log("PnP state of the device: {0}".format(pnp_state), "INFO") @@ -953,6 +956,10 @@ class instance for further use. return self update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} + if is_stack is True: + update_payload["deviceInfo"]["stack"] = True + + self.log("The request sent for 'update_device' API for device's config update: {0}".format(update_payload), "DEBUG") update_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="update_device", From a9a93acbe0096cac854b996878bce10a282b3042 Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 7 Mar 2024 07:10:18 +0000 Subject: [PATCH 42/44] Adding fix for Stackswitch getting changed to normal switch post deiting the device's info --- plugins/modules/pnp_intent.py | 3 +-- plugins/modules/pnp_workflow_manager.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index a0926d1ec4..35c7268a3e 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -956,8 +956,7 @@ class instance for further use. return self update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} - if is_stack is True: - update_payload["deviceInfo"]["stack"] = True + update_payload["deviceInfo"]["stack"] = is_stack self.log("The request sent for 'update_device' API for device's config update: {0}".format(update_payload), "DEBUG") update_response = self.dnac_apply['exec']( diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 15149953fa..b402b5f15f 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -956,8 +956,7 @@ class instance for further use. return self update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} - if is_stack is True: - update_payload["deviceInfo"]["stack"] = True + update_payload["deviceInfo"]["stack"] = is_stack self.log("The request sent for 'update_device' API for device's config update: {0}".format(update_payload), "DEBUG") update_response = self.dnac_apply['exec']( From 682cfd3cf15ebc34dbd11d909ce956ef4a025aa2 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 7 Mar 2024 22:14:44 +0530 Subject: [PATCH 43/44] Updated the changelog.yaml about the stackswitch getting converted to normal switch --- changelogs/changelog.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 909e4cdcff..64df79e0e0 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -827,3 +827,5 @@ releases: attributes 'clear_mac_address_table', 'device_ip', 'resync_retry_count', 'resync_retry_interval', 'reprovision_wired_device', 'provision_wireless_device' were added. Renamed argument from 'ip_address' to 'ip_address_list'. + - pnp_workflow_manager - Adding fix for Stackswitch getting changed to normal switch post editing the device's info. + - pnp_intent - Adding fix for Stackswitch getting changed to normal switch post editing the device's info. From 9d9f353347b3113f0895155151a38eda1da11b35 Mon Sep 17 00:00:00 2001 From: bvargasre Date: Thu, 7 Mar 2024 12:51:55 -0600 Subject: [PATCH 44/44] Update supported versions in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1ab07480c..2a3014b43c 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.11.0 | 2.6.0 | +| 2.3.5.3 | 6.12.0 | 2.6.0 | If your Ansible collection is older please consider updating it first.