From 92e0eabb841d0c8b2e31f41781e9e935a0c6bb00 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Mon, 21 Oct 2024 14:13:11 -0700 Subject: [PATCH] addressed PR review comments --- plugins/module_utils/dnac.py | 100 +++++++++--- .../device_configs_backup_workflow_manager.py | 16 +- .../network_compliance_workflow_manager.py | 153 ++++++++++++++---- 3 files changed, 208 insertions(+), 61 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 4f40717356..d670cc083f 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -392,16 +392,15 @@ def get_task_details(self, task_id): params={"task_id": task_id}, op_modifies=True, ) - self.log("Retrieving task details by the API 'get_task_by_id' using task ID: {task_id}, Response: {response}" - .format(task_id=task_id, response=response), "DEBUG") - self.log('Task Details: {response}'.format(response=response), 'DEBUG') + self.log("Retrieving task details by the API 'get_task_by_id' using task ID: {0}, Response: {1}" + .format(task_id, response), "DEBUG") if not isinstance(response, dict): self.log("Failed to retrieve task details for task ID: {}".format(task_id), "ERROR") return task_status task_status = response.get('response') - self.log("Task Status: {task_status}".format(task_status=task_status), "DEBUG") + self.log("Successfully retrieved Task status: {0}".format(task_status), "DEBUG") except Exception as e: # Log an error message and fail if an exception occurs self.log_traceback() @@ -522,9 +521,8 @@ def get_execution_details(self, execid): function='get_business_api_execution_details', params={"execution_id": execid} ) - self.log("Retrieving execution details by the API 'get_business_api_execution_details' using exec ID: {0}, Response: {1}" + self.log("Successfully retrieved execution details by the API 'get_business_api_execution_details' for execution ID: {0}, Response: {1}" .format(execid, response), "DEBUG") - self.log('Execution Details: {0}'.format(response), 'DEBUG') except Exception as e: # Log an error message and fail if an exception occurs self.log_traceback() @@ -631,12 +629,12 @@ def get_sites_type(self, site_name): site_type = site[0].get("type") except Exception as e: - self.msg = "An exception occurred: while fetching the site '{0}'. Error: {1}".format(site_name, e) + self.msg = "An exception occurred while fetching the site '{0}'. Error: {1}".format(site_name, e) self.fail_and_exit(self.msg) return site_type - def get_device_ids_from_site(self, site_id): + def get_device_ids_from_site(self, site_name, site_id=None): """ Retrieve device IDs associated with a specific site in Cisco Catalyst Center. Args: @@ -648,13 +646,26 @@ def get_device_ids_from_site(self, site_id): device_ids = [] api_response = None + + # If site_id is not provided, retrieve it based on the site_name + if site_id is None: + self.log("Site ID not provided. Retrieving Site ID for site name: '{0}'.".format(site_name), "DEBUG") + site_id, site_exists = self.get_site_id(site_name) + if not site_exists: + self.log("Site '{0}' does not exist, cannot proceed with device retrieval.".format(site_name), "ERROR") + return api_response, device_ids + + self.log("Retrieved site ID '{0}' for site name '{1}'.".format(site_id, site_name), "DEBUG") + self.log("Initiating retrieval of device IDs for site ID: '{0}'.".format(site_id), "DEBUG") # Determine API based on dnac_version if self.dnac_version <= self.version_2_3_5_3: + self.log("Using 'get_membership' API for Catalyst Center version: '{0}'.".format(self.dnac_version), "DEBUG") get_membership_params = {"site_id": site_id} api_response = self.execute_get_request("sites", "get_membership", get_membership_params) + self.log("Received response from 'get_membership'. Extracting device IDs.", "DEBUG") if api_response and "device" in api_response: for device in api_response.get("device", []): for item in device.get("response", []): @@ -662,9 +673,11 @@ def get_device_ids_from_site(self, site_id): self.log("Retrieved device IDs from membership for site '{0}': {1}".format(site_id, device_ids), "DEBUG") else: + self.log("Using 'get_site_assigned_network_devices' API for DNAC version: '{0}'.".format(self.dnac_version), "DEBUG") get_site_assigned_network_devices_params = {"site_id": site_id} api_response = self.execute_get_request("site_design", "get_site_assigned_network_devices", get_site_assigned_network_devices_params) + self.log("Received response from 'get_site_assigned_network_devices'. Extracting device IDs.", "DEBUG") if api_response and "response" in api_response: for device in api_response.get("response", []): device_ids.append(device.get("deviceId")) @@ -672,11 +685,11 @@ def get_device_ids_from_site(self, site_id): self.log("Retrieved device IDs from assigned devices for site '{0}': {1}".format(site_id, device_ids), "DEBUG") if not device_ids: - self.log("No devices found for site '{0}'".format(site_id), "INFO") + self.log("No devices found for site '{0}' with site ID: '{1}'.".format(site_name, site_id), "WARNING") return api_response, device_ids - def get_device_details_from_site(self, site_id): + def get_device_details_from_site(self, site_name, site_id=None): """ Retrieves device details for all devices within a specified site. Args: @@ -689,8 +702,18 @@ def get_device_details_from_site(self, site_id): device_details_list = [] self.log("Initiating retrieval of device IDs for site ID: '{0}'.".format(site_id), "INFO") + # If site_id is not provided, retrieve it based on the site_name + if site_id is None: + self.log("Site ID not provided. Retrieving Site ID for site name: '{0}'.".format(site_name), "DEBUG") + site_id, site_exists = self.get_site_id(site_name) + if not site_exists: + self.log("Site '{0}' does not exist, cannot proceed with device retrieval.".format(site_name), "ERROR") + return api_response, device_ids + + self.log("Retrieved site ID '{0}' for site name '{1}'.".format(site_id, site_name), "DEBUG") + # Retrieve device IDs from the specified site - api_response, device_ids = self.get_device_ids_from_site(site_id) + api_response, device_ids = self.get_device_ids_from_site(site_name, site_id) if not api_response: self.msg = "No response received from API call 'get_device_ids_from_site' for site ID: {0}".format(site_id) self.fail_and_exit(self.msg) @@ -715,7 +738,7 @@ def get_device_details_from_site(self, site_id): return device_details_list - def get_reachable_devices_from_site(self, site_id): + def get_reachable_devices_from_site(self, site_name): """ Retrieves a mapping of management IP addresses to instance IDs for reachable devices from a specified site. Args: @@ -728,10 +751,15 @@ def get_reachable_devices_from_site(self, site_id): mgmt_ip_to_instance_id_map = {} skipped_devices_list = [] + (site_exists, site_id) = self.get_site_id(site_name) + if not site_exists: + self.msg = "Site '{0}' does not exist in the Cisco Catalyst Center, cannot proceed with device(s) retrieval.".format(site_name) + self.fail_and_exit(self.msg) + self.log("Initiating retrieval of device details for site ID: '{0}'.".format(site_id), "INFO") # Retrieve the list of device details from the specified site - device_details_list = self.get_device_details_from_site(site_id) + device_details_list = self.get_device_details_from_site(site_name, site_id) self.log("Device details retrieved for site ID: '{0}': {1}".format(site_id, device_details_list), "DEBUG") # Iterate through each device's details @@ -782,9 +810,11 @@ def get_site(self, site_name): # Determine API call based on dnac_version if self.dnac_version <= self.version_2_3_5_3: + self.log("Using 'get_site' API for Catalyst Center version: '{0}'.".format(self.dnac_version), "DEBUG") get_site_params = {"name": site_name} response = self.execute_get_request("sites", "get_site", get_site_params) else: + self.log("Using 'get_sites' API for Catalyst Center version: '{0}'.".format(self.dnac_version), "DEBUG") get_sites_params = {"name_hierarchy": site_name} response = self.execute_get_request("site_design", "get_sites", get_sites_params) @@ -818,7 +848,7 @@ def get_site_id(self, site_name): site_exists = True except Exception as e: - self.msg = "An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center. Error: {1}".format(site_name, e) + self.msg = "An exception occurred while retrieving Site details for Site '{0}' does not exist in the Cisco Catalyst Center. Error: {1}".format(site_name, e) self.fail_and_exit(self.msg) return (site_exists, site_id) @@ -1449,7 +1479,7 @@ def execute_get_request(self, api_family, api_function, api_parameters): Logs detailed information about the API call, including responses and errors. """ self.log( - "Initiating GET API call for Function: {0} from family: {1} with Parameters: {2}.".format( + "Initiating GET API call for Function: {0} from Family: {1} with Parameters: {2}.".format( api_function, api_family, api_parameters ), "DEBUG" @@ -1570,21 +1600,26 @@ def get_task_status_from_tasks_by_id(self, task_id, task_name, success_msg): self: The instance of the class with updated status and message. """ loop_start_time = time.time() + self.log("Starting task monitoring for '{0}' with task ID '{1}'.".format(task_name, task_id), "DEBUG") while True: response = self.get_tasks_by_id(task_id) # Check if response is returned if not response: - self.msg = "Error retrieving task status for '{0}' with task_id '{1}'".format(task_name, task_id) + self.msg = "Error retrieving task status for '{0}' with task ID '{1}'".format(task_name, task_id) self.set_operation_result("failed", False, self.msg, "ERROR") break + self.log("Successfully retrieved task details: {0}".format(response), "INFO") + status = response.get("status") end_time = response.get("endTime") + elapsed_time = time.time() - loop_start_time # Check if the elapsed time exceeds the timeout if self.check_timeout_and_exit(loop_start_time, task_id, task_name): + self.log("Timeout exceeded after {0:.2f} seconds while monitoring task '{1}' with task ID '{2}'.".format(elapsed_time, task_name, task_id), "DEBUG") break # Check if the task has completed (either success or failure) @@ -1609,8 +1644,12 @@ def get_task_status_from_tasks_by_id(self, task_id, task_name, success_msg): break # Wait for the specified poll interval before the next check - time.sleep(self.params.get("dnac_task_poll_interval")) + poll_interval = self.params.get("dnac_task_poll_interval") + self.log("Waiting for the next poll interval of {0} seconds before checking task status again.".format(poll_interval), "DEBUG") + time.sleep(poll_interval) + total_elapsed_time = time.time() - loop_start_time + self.log("Completed monitoring task '{0}' with task ID '{1}' after {2:.2f} seconds.".format(task_name, task_id, total_elapsed_time), "DEBUG") return self def get_task_status_from_task_by_id(self, task_id, task_name, failure_msg, success_msg, progress_validation=None, data_validation=None): @@ -1627,13 +1666,15 @@ def get_task_status_from_task_by_id(self, task_id, task_name, failure_msg, succe self: The instance of the class. """ loop_start_time = time.time() + self.log("Starting task monitoring for '{0}' with task ID '{1}'.".format(task_name, task_id), "DEBUG") + while True: # Retrieve task details by task ID response = self.get_task_details(task_id) # Check if response is returned if not response: - self.msg = "Error retrieving task status for '{0}' with task_id '{1}'".format(task_name, task_id) + self.msg = "Error retrieving task status for '{0}' with task ID '{1}'".format(task_name, task_id) self.set_operation_result("failed", False, self.msg, "ERROR") break @@ -1646,32 +1687,41 @@ def get_task_status_from_task_by_id(self, task_id, task_name, failure_msg, succe self.set_operation_result("failed", False, self.msg, "ERROR") break + self.log("Successfully retrieved task details: {0}".format(response), "INFO") + # Check if the elapsed time exceeds the timeout + elapsed_time = time.time() - loop_start_time if self.check_timeout_and_exit(loop_start_time, task_id, task_name): + self.log("Timeout exceeded after {0:.2f} seconds while monitoring task '{1}' with task ID '{2}'.".format(elapsed_time, task_name, task_id), "DEBUG") break # Extract data, progress, and end time from the response data = response.get("data") progress = response.get("progress") end_time = response.get("endTime") + self.log("Current task progress for '{0}': {1}, Data: {2}".format(task_name, progress, data), "INFO") - # Validate task data if data_validation key is provided - if data_validation: - if end_time and data_validation in data: + # Validate task data or progress if validation keys are provided + if end_time: + if data_validation and data_validation in data: self.msg = success_msg self.set_operation_result("success", True, self.msg, "INFO") + self.log(self.msg, "INFO") break - # Validate task progress if progress_validation key is provided - if progress_validation: - if end_time and progress_validation in progress: + if progress_validation and progress_validation in progress: self.msg = success_msg self.set_operation_result("success", True, self.msg, "INFO") + self.log(self.msg, "INFO") break # Wait for the specified poll interval before the next check - time.sleep(self.params.get("dnac_task_poll_interval")) + poll_interval = self.params.get("dnac_task_poll_interval") + self.log("Waiting for the next poll interval of {0} seconds before checking task status again.".format(poll_interval), "DEBUG") + time.sleep(poll_interval) + total_elapsed_time = time.time() - loop_start_time + self.log("Completed monitoring task '{0}' with task ID '{1}' after {2:.2f} seconds.".format(task_name, task_id, total_elapsed_time), "DEBUG") return self def requires_update(self, have, want, obj_params): diff --git a/plugins/modules/device_configs_backup_workflow_manager.py b/plugins/modules/device_configs_backup_workflow_manager.py index d85f79d857..b8118c01e0 100644 --- a/plugins/modules/device_configs_backup_workflow_manager.py +++ b/plugins/modules/device_configs_backup_workflow_manager.py @@ -615,14 +615,10 @@ def get_device_id_list(self, config): # Retrieve device IDs for each site in the unique_sites set for site_name in unique_sites: - (site_exists, site_id) = self.get_site_id(site_name) - if site_exists: - site_mgmt_ip_to_instance_id_map, skipped_devices_list = self.get_reachable_devices_from_site(site_id) - self.log("Retrieved following Device Id(s) of device(s): {0} from the provided site: {1}".format( - site_mgmt_ip_to_instance_id_map, site_name), "DEBUG") - mgmt_ip_to_instance_id_map.update(site_mgmt_ip_to_instance_id_map) - else: - self.log("Site '{0}' does not exist.".format(site_name), "WARNING") + site_mgmt_ip_to_instance_id_map, skipped_devices_list = self.get_reachable_devices_from_site(site_name) + self.log("Retrieved following Device Id(s) of device(s): {0} from the provided site: {1}".format( + site_mgmt_ip_to_instance_id_map, site_name), "DEBUG") + mgmt_ip_to_instance_id_map.update(site_mgmt_ip_to_instance_id_map) # Get additional device list parameters excluding site_list get_device_list_params = self.get_device_list_params(config) @@ -900,6 +896,7 @@ def get_export_device_config_task_status(self, task_id): self.get_task_status_from_tasks_by_id(task_id, task_name, success_msg) if self.status == "success": + self.log("Task '{0}' completed successfully for task ID {1}.".format(task_name, task_id), "INFO") if self.dnac_version <= self.version_2_3_5_3: response = self.get_task_details(task_id) additional_status_url = response.get("additionalStatusURL") @@ -910,11 +907,13 @@ def get_export_device_config_task_status(self, task_id): if not additional_status_url: self.msg = "Error retrieving the Device Config Backup file ID for task ID {0}".format(task_id) self.fail_and_exit(self.msg) + self.log("Additional status URL retrieved: {0}".format(additional_status_url), "DEBUG") # Perform additional tasks after breaking the loop mgmt_ip_to_instance_id_map = self.want.get("mgmt_ip_to_instance_id_map") # Download the file using the additional status URL + self.log("Downloading the Device Config Backup file from {0}.".format(additional_status_url), "DEBUG") file_id, downloaded_file = self.download_file(additional_status_url=additional_status_url) self.log("Retrived file data for file ID: {0}.".format(file_id), "DEBUG") if not downloaded_file: @@ -922,6 +921,7 @@ def get_export_device_config_task_status(self, task_id): self.fail_and_exit(self.msg) # Unzip the downloaded file + self.log("Unzipping the downloaded Device Config Backup file(s) for file ID: {0}.".format(file_id), "DEBUG") download_status = self.unzip_data(file_id, downloaded_file) if download_status: self.log("{0} task has been successfully performed on {1} device(s): {2}.".format( diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 19d3192f9a..74195970e5 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -461,7 +461,7 @@ def validate_iplist_and_site_name(self, ip_address_list, site_name): # Check if IP address list or hostname is provided if not any([ip_address_list, site_name]): - self.msg = "Provided 'ip_address_list': {0}, 'site_name': {1}. Either 'ip_address_list' or 'site_name' must be provided.".format( + self.msg = "Error: Neither 'ip_address_list' nor 'site_name' was provided. Provided values: 'ip_address_list': {0}, 'site_name': {1}.".format( ip_address_list, site_name) self.fail_and_exit(self.msg) @@ -472,16 +472,37 @@ def validate_iplist_and_site_name(self, ip_address_list, site_name): self.log("Validation successful: Provided IP address list or Site name is valid") def validate_compliance_operation(self, run_compliance, run_compliance_categories, sync_device_config): - self.log("Validating if any network compliance operation requested", "DEBUG") + """ + Validates if any network compliance operation is requested. + Args: + run_compliance (bool): Indicates if a compliance check operation is requested. + run_compliance_categories (list): A list of compliance categories to be checked. + sync_device_config (bool): Indicates if a device configuration synchronization is requested. + Raises: + Exception: If no compliance operation is requested, raises an exception with a message. + """ + self.log("Validating if any network compliance operation is requested: " + "run_compliance={0}, run_compliance_categories={1}, sync_device_config={2}".format( + run_compliance, run_compliance_categories, sync_device_config), "DEBUG") if not any([run_compliance, run_compliance_categories, sync_device_config]): - self.msg = "No actions were requested. This network compliance module can perform the following tasks: Run Compliance Check or Sync Device Config." + self.msg = ( + "No actions were requested. This network compliance module can perform the following tasks: " + "Run Compliance Check or Sync Device Config." + ) self.fail_and_exit(self.msg) self.log("Validation successful: Network Compliance operation present") def validate_run_compliance_categories(self, run_compliance_categories): - self.log("Validating the provided Run Compliance categories", "DEBUG") + """ + Validates the provided Run Compliance categories. + Args: + run_compliance_categories (list): A list of compliance categories to be checked. + Raises: + Exception: If invalid categories are provided, raises an exception with a message. + """ + self.log("Validating the provided run compliance categories: {0}".format(run_compliance_categories), "DEBUG") valid_categories = ["INTENT", "RUNNING_CONFIG", "IMAGE", "PSIRT", "EOX", "NETWORK_SETTINGS"] if not all(category.upper() in valid_categories for category in run_compliance_categories): @@ -492,12 +513,30 @@ def validate_run_compliance_categories(self, run_compliance_categories): self.log("Validation successful: valid run compliance categorites provided: {0}".format(run_compliance_categories), "DEBUG") def validate_params(self, config): - self.log("Validating the provided configuration", "INFO") + """ + Validates the provided configuration for network compliance operations. + Args: + config (dict): A dictionary containing the configuration parameters. + Validations: + - Ensures that either ip_address_list or site_name is provided. + - Checks if a network compliance operation is requested. + - Validates the compliance categories if provided. + Raises: + Exception: If any validation fails, raises an exception with a message. + """ + self.log("Validating the provided configuration: {0}".format(config), "INFO") ip_address_list = config.get("ip_address_list") site_name = config.get("site_name") run_compliance = config.get("run_compliance") run_compliance_categories = config.get("run_compliance_categories") sync_device_config = config.get("sync_device_config") + self.log( + "Extracted parameters - IP Address List: {0}, Site Name: {1}, Run Compliance: {2}, " + "Run Compliance Categories: {3}, Sync Device Config: {4}".format( + ip_address_list, site_name, run_compliance, run_compliance_categories, sync_device_config + ), + "DEBUG" + ) # Validate either ip_address_list OR site_name is present self.validate_iplist_and_site_name(ip_address_list, site_name) @@ -506,7 +545,6 @@ def validate_params(self, config): self.validate_compliance_operation(run_compliance, run_compliance_categories, sync_device_config) # Validate the categories if provided - run_compliance_categories = config.get("run_compliance_categories") if run_compliance_categories: self.validate_run_compliance_categories(run_compliance_categories) @@ -575,6 +613,23 @@ def get_run_compliance_params(self, mgmt_ip_to_instance_id_map, run_compliance, return run_compliance_params def get_sync_device_config_params(self, mgmt_ip_to_instance_id_map, categorized_devices): + """ + Generates parameters for syncing device configurations, excluding compliant and other categorized devices. + Args: + mgmt_ip_to_instance_id_map (dict): A dictionary mapping management IP addresses to instance IDs of devices. + categorized_devices (dict): A dictionary categorizing devices by their compliance status. + Returns: + dict: A dictionary containing the device IDs to be used for syncing device configurations. + Description: + This method generates a dictionary of parameters required for syncing device configurations. It initially includes all device + IDs from `mgmt_ip_to_instance_id_map`. It then excludes devices categorized as "OTHER" or "COMPLIANT" from the sync operation. + The excluded devices' IPs are logged and added to the `skipped_sync_device_configs_list`. The updated list of device IDs to be synced + is returned. + """ + self.log("Entering get_sync_device_config_params method with mgmt_ip_to_instance_id_map: {0}, categorized_devices: {1}".format( + mgmt_ip_to_instance_id_map, categorized_devices), "DEBUG" + ) + sync_device_config_params = { "deviceId": list(mgmt_ip_to_instance_id_map.values()) } @@ -583,6 +638,10 @@ def get_sync_device_config_params(self, mgmt_ip_to_instance_id_map, categorized_ compliant_device_ips = categorized_devices.get("COMPLIANT", {}).keys() excluded_device_ips = set(other_device_ips) | set(compliant_device_ips) + self.log("Identified other device IPs: {0}".format(", ".join(other_device_ips)), "DEBUG") + self.log("Identified compliant device IPs: {0}".format(", ".join(compliant_device_ips)), "DEBUG") + self.log("Identified excluded device IPs: {0}".format(", ".join(excluded_device_ips)), "DEBUG") + if excluded_device_ips: self.skipped_sync_device_configs_list.extend(excluded_device_ips) excluded_device_uuids = [mgmt_ip_to_instance_id_map[ip] for ip in excluded_device_ips if ip in mgmt_ip_to_instance_id_map] @@ -595,7 +654,7 @@ def get_sync_device_config_params(self, mgmt_ip_to_instance_id_map, categorized_ self.log(msg, "WARNING") self.log("Updated 'sync_device_config_params' parameters: {0}".format(sync_device_config_params), "DEBUG") - self.log("sync_device_config_params: {0}".format(sync_device_config_params), "DEBUG") + self.log("Final sync_device_config_params: {0}".format(sync_device_config_params), "DEBUG") return sync_device_config_params def get_device_list_params(self, ip_address_list): @@ -610,10 +669,12 @@ def get_device_list_params(self, ip_address_list): names required by Cisco Catalyst Center. It returns a dictionary of these mapped parameters which can be used to query devices based on the provided filters. """ + self.log("Entering get_device_list_params method with ip_address_list: {0}".format(ip_address_list), "DEBUG") + # Initialize an empty dictionary to store the mapped parameters get_device_list_params = {"management_ip_address": ip_address_list} - self.log("get_device_list_params: {0}".format(get_device_list_params), "DEBUG") + self.log("Generated get_device_list_params: {0}".format(get_device_list_params), "DEBUG") return get_device_list_params def get_device_ids_from_ip(self, get_device_list_params): @@ -628,6 +689,8 @@ def get_device_ids_from_ip(self, get_device_list_params): mapped to their instance IDs. Logs detailed information about the number of devices processed, skipped, and the final list of devices available for configuration backup. """ + self.log("Entering 'get_device_ids_from_ip' method with parameters: {0}".format(get_device_list_params), "DEBUG") + mgmt_ip_to_instance_id_map = {} processed_device_count = 0 skipped_device_count = 0 @@ -646,7 +709,7 @@ def get_device_ids_from_ip(self, get_device_list_params): response = self.dnac._exec( family="devices", function="get_device_list", - op_modifies=True, + op_modifies=False, params=get_device_list_params ) self.log("Response received post 'get_device_list' API call with offset {0}: {1}".format(offset, str(response)), "DEBUG") @@ -660,12 +723,24 @@ def get_device_ids_from_ip(self, get_device_list_params): for device_info in response.get("response", []): processed_device_count += 1 device_ip = device_info.get("managementIpAddress", "Unknown IP") + reachability_status = device_info.get("reachabilityStatus") + collection_status = device_info.get("collectionStatus") + device_family = device_info.get("family") + device_id = device_info.get("id") + self.log( + "Processing device with IP: {0}, Reachability: {1}, Collection Status: {2}, Family: {3}".format( + device_ip, + reachability_status, + collection_status, + device_family + ), + "DEBUG" + ) # Check if the device is reachable and managed - if device_info.get("reachabilityStatus") == "Reachable" and device_info.get("collectionStatus") == "Managed": + if reachability_status == "Reachable" and collection_status == "Managed": # Skip Unified AP devices - if device_info.get("family") != "Unified AP" : - device_id = device_info["id"] + if device_family != "Unified AP" : mgmt_ip_to_instance_id_map[device_ip] = device_id else: skipped_device_count += 1 @@ -673,7 +748,7 @@ def get_device_ids_from_ip(self, get_device_list_params): self.skipped_sync_device_configs_list.append(device_ip) msg = ( "Skipping device {0} as its family is: {1}.".format( - device_ip, device_info.get("family") + device_ip, device_family ) ) self.log(msg, "INFO") @@ -684,7 +759,7 @@ def get_device_ids_from_ip(self, get_device_list_params): skipped_device_count += 1 msg = ( "Skipping device {0} as its status is {1} or its collectionStatus is {2}.".format( - device_ip, device_info.get("reachabilityStatus"), device_info.get("collectionStatus") + device_ip, reachability_status, collection_status ) ) self.log(msg, "INFO") @@ -726,24 +801,25 @@ def get_device_id_list(self, ip_address_list, site_name): This method queries Cisco Catalyst Center to retrieve the unique device IDs associated with devices having the specified IP addresses or belonging to the specified site. """ + self.log("Entering get_device_id_list with args: ip_address_list={0}, site_name={1}".format( + ip_address_list, site_name), "DEBUG") + # Initialize a dictionary to store management IP addresses and their corresponding device IDs mgmt_ip_to_instance_id_map = {} if ip_address_list: - # Retrieve device IDs associated with devices having specified IP addresses + self.log("Retrieving device IDs for IP addresses: {0}".format(", ".join(ip_address_list)), "DEBUG") get_device_list_params = self.get_device_list_params(ip_address_list) iplist_mgmt_ip_to_instance_id_map = self.get_device_ids_from_ip(get_device_list_params) mgmt_ip_to_instance_id_map.update(iplist_mgmt_ip_to_instance_id_map) # Check if both site name and IP address list are provided if site_name: - (site_exists, site_id) = self.get_site_id(site_name) - if site_exists: - # Retrieve device IDs associated with devices in the site - site_mgmt_ip_to_instance_id_map, skipped_devices_list = self.get_reachable_devices_from_site(site_id) - self.skipped_run_compliance_devices_list.extend(skipped_devices_list) - self.skipped_sync_device_configs_list.extend(skipped_devices_list) - mgmt_ip_to_instance_id_map.update(site_mgmt_ip_to_instance_id_map) + self.log("Retrieving device IDs for site: {0}".format(site_name), "DEBUG") + site_mgmt_ip_to_instance_id_map, skipped_devices_list = self.get_reachable_devices_from_site(site_name) + self.skipped_run_compliance_devices_list.extend(skipped_devices_list) + self.skipped_sync_device_configs_list.extend(skipped_devices_list) + mgmt_ip_to_instance_id_map.update(site_mgmt_ip_to_instance_id_map) if not mgmt_ip_to_instance_id_map: # Log an error message if mgmt_ip_to_instance_id_map is empty @@ -1018,10 +1094,6 @@ def get_batches_result(self, batches_dict): task_status = self.status self.log("The task status of batch: {0} with task id: {1} is {2}".format(idx, task_id, task_status), "INFO") - # # Extract message and status from the task status result - # msg = task_status.status - # status = task_status[task_id]["status"] - # Store the result for the current batch batch_result = { "task_id": task_id, @@ -1169,17 +1241,22 @@ def get_sync_config_task_status(self, task_id, mgmt_ip_to_instance_id_map): It continuously checks the task status until completion, updating the result accordingly. """ task_name = "Sync Device Configuration" - + self.log("Entering '{0}' with task_id: '{1}' and mgmt_ip_to_instance_id_map: {2}".format( + task_name, task_id, mgmt_ip_to_instance_id_map), "INFO" + ) msg = {} # Retrieve the parameters for sync device config sync_device_config_params = self.want.get("sync_device_config_params") + self.log("Sync device config parameters: {0}".format(sync_device_config_params), "DEBUG") # Extract the list of device IDs from sync_device_config_params device_ids = sync_device_config_params.get("deviceId") + self.log("Device IDs for synchronization: {0}".format(device_ids), "DEBUG") # Create device_ip_list by mapping the device IDs back to their corresponding IP addresses device_ip_list = [ip for ip, device_id in mgmt_ip_to_instance_id_map.items() if device_id in device_ids] + self.log("Device IPs to synchronize: {0}".format(device_ip_list), "DEBUG") msg["{0} Succeeded for following device(s)".format(task_name)] = {"success_count": len(device_ip_list), "success_devices": device_ip_list} @@ -1244,18 +1321,31 @@ def verify_sync_device_config(self): all_statuses_after = [] # Iterate over the device IDs and check their compliance status + self.log("Device IDs to check: {0}".format(sync_device_ids), "DEBUG") for device_id in sync_device_ids: # Find the corresponding IP address from the mgmt_ip_to_instance_id_map ip_address = next((ip for ip, id in self.want.get("mgmt_ip_to_instance_id_map").items() if id == device_id), None) + self.log("Found IP address for device ID {0}: {1}".format(device_id, ip_address), "DEBUG") if ip_address: + self.log("Checking compliance status for device ID: {0}".format(device_id), "DEBUG") # Get the status before compliance_before = compliance_details_before.get(ip_address, []) if compliance_before: all_statuses_before.append(compliance_before[0]["status"]) + else: + self.log("No compliance details found for device IP: {0} before synchronization.".format(ip_address), "DEBUG") # Get the status after compliance_after = compliance_details_after.get(ip_address, []) if compliance_after: all_statuses_after.append(compliance_after[0]["status"]) + else: + self.log("No compliance details found for device IP: {0} after synchronization.".format(ip_address), "DEBUG") + + self.log("Compliance statuses before synchronization: {0}".format(all_statuses_before), "DEBUG") + self.log("Compliance statuses after synchronization: {0}".format(all_statuses_after), "DEBUG") + + else: + self.log("No IP address found for device ID: {0}".format(device_id), "DEBUG") # Check if all statuses changed from "NON_COMPLIANT" to "COMPLIANT" if ( @@ -1290,9 +1380,11 @@ def get_want(self, config): # Store input parameters ip_address_list = config.get("ip_address_list") + self.log("Original IP address list: {0}".format(ip_address_list), "DEBUG") # Remove Duplicates from list if ip_address_list: ip_address_list = list(set(ip_address_list)) + self.log("Deduplicated IP address list: {0}".format(ip_address_list), "DEBUG") site_name = config.get("site_name") run_compliance = config.get("run_compliance") @@ -1304,19 +1396,22 @@ def get_want(self, config): # Retrieve device ID list mgmt_ip_to_instance_id_map = self.get_device_id_list(ip_address_list, site_name) + self.log("Management IP to Instance ID Map: {0}".format(mgmt_ip_to_instance_id_map), "DEBUG") # Run Compliance Paramters run_compliance_params = self.get_run_compliance_params(mgmt_ip_to_instance_id_map, run_compliance, run_compliance_categories) # Sync Device Configuration Parameters if sync_device_config: + self.log("Sync Device Configuration is requested.", "DEBUG") if self.dnac_version > self.version_2_3_5_3: compliance_detail_params_sync = { "deviceUuids": list(mgmt_ip_to_instance_id_map.values()), "categories": ["RUNNING_CONFIG"] } - + self.log("Retrieving compliance report with parameters: {0}".format(compliance_detail_params_sync), "DEBUG") response = self.get_compliance_report(compliance_detail_params_sync, mgmt_ip_to_instance_id_map) + self.log("Response from get_compliance_report: {0}".format(response), "DEBUG") if not response: ip_address_list_str = ", ".join(list(mgmt_ip_to_instance_id_map.keys())) self.msg = "Error occurred when retrieving Compliance Report to identify if Sync Device Config Operation " @@ -1417,6 +1512,7 @@ def get_diff_merged(self, config): if config.get("sync_device_config"): skipped_sync_device_configs_list = set(self.skipped_sync_device_configs_list) if skipped_sync_device_configs_list: + self.log("Sync Device Configuration skipped for devices: {0}".format(skipped_sync_device_configs_list), "DEBUG") result_details["Sync Device Configuration operation Skipped for following device(s)"] = { "skipped_count": len(skipped_sync_device_configs_list), "skipped_devices": skipped_sync_device_configs_list @@ -1424,6 +1520,7 @@ def get_diff_merged(self, config): skipped_run_compliance_devices_list = set(self.skipped_run_compliance_devices_list) if skipped_run_compliance_devices_list: + self.log("Run Compliance Check skipped for devices: {0}".format(skipped_run_compliance_devices_list), "DEBUG") result_details["Run Compliance Check Skipped for following device(s)"] = { "skipped_count": len(skipped_run_compliance_devices_list), "skipped_devices": skipped_run_compliance_devices_list