From 62f65d0e158b49da96bc37b39352b69880735b16 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sat, 19 Oct 2024 17:36:56 -0700 Subject: [PATCH 1/7] network compliance, device config backups bug fixes and dnac.py update --- plugins/module_utils/dnac.py | 592 ++++++----- .../device_configs_backup_workflow_manager.py | 138 +-- .../network_compliance_workflow_manager.py | 983 ++++++++++-------- .../test_device_configs_backup_management.yml | 17 +- 4 files changed, 958 insertions(+), 772 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index e17c3df305..a3f312a4b2 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -384,26 +384,39 @@ def get_task_details(self, task_id): Description: If the task with the specified task ID is not found in Cisco Catalyst Center, this function will return None. """ + task_status = None + try: + response = self.dnac._exec( + family="task", + function='get_task_by_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') - result = None - response = self.dnac._exec( - family="task", - function='get_task_by_id', - params={"task_id": task_id} - ) - - self.log('Task Details: {0}'.format(str(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 - if response and isinstance(response, dict): - result = response.get('response') + task_status = response.get('response') + self.log("Task Status: {task_status}".format(task_status=task_status), "DEBUG") + except Exception as e: + # Log an error message and fail if an exception occurs + self.log_traceback() + self.msg = ( + "An error occurred while executing API call to Function: 'get_tasks_by_id' " + "due to the the following exception: {0}.".format(str(e)) + ) + self.fail_and_exit(self.msg) - return result + return task_status def get_device_details_limit(self): """ Retrieves the limit for 'get_device_list' API to collect the device details.. - Parameters: + Args: self (object): An instance of a class that provides access to Cisco Catalyst Center. Returns: int: The limit for 'get_device_list' api device details, which is set to 500 by default. @@ -418,14 +431,12 @@ def get_device_details_limit(self): def check_task_response_status(self, response, validation_string, api_name, data=False): """ Get the site id from the site name. - - Parameters: + Args: self - The current object details. response (dict) - API response. validation_string (str) - String used to match the progress status. api_name (str) - API name. data (bool) - Set to True if the API is returning any information. Else, False. - Returns: self """ @@ -499,35 +510,40 @@ def reset_values(self): def get_execution_details(self, execid): """ Get the execution details of an API - - Parameters: + Args: execid (str) - Id for API execution - Returns: response (dict) - Status for API execution """ - - self.log("Execution Id: {0}".format(execid), "DEBUG") - response = self.dnac._exec( - family="task", - function='get_business_api_execution_details', - params={"execution_id": execid} - ) - self.log("Response for the current execution: {0}".format(response)) + response = None + try: + response = self.dnac._exec( + family="task", + 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}" + .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() + self.msg = ( + "An error occurred while executing API call to Function: 'get_business_api_execution_details' " + "due to the the following exception: {0}.".format(str(e)) + ) + self.fail_and_exit(self.msg) return response def check_execution_response_status(self, response, api_name): """ Checks the reponse status provided by API in the Cisco Catalyst Center - - Parameters: + Args: response (dict) - API response api_name (str) - API name - Returns: self """ - if not response: self.msg = ( "The response from the API '{api_name}' is empty." @@ -573,10 +589,8 @@ def check_execution_response_status(self, response, api_name): def check_string_dictionary(self, task_details_data): """ Check whether the input is string dictionary or string. - - Parameters: + Args: task_details_data (string) - Input either string dictionary or string. - Returns: value (dict) - If the input is string dictionary, else returns None. """ @@ -589,55 +603,10 @@ def check_string_dictionary(self, task_details_data): pass return None - def get_device_ip_from_device_id(self, site_id): - """ - Retrieve the management IP addresses and their corresponding instance UUIDs of devices associated with a specific site in Cisco Catalyst Center. - - Args: - site_id (str): The ID of the site to be retrieved. - - Returns: - dict: A dictionary mapping management IP addresses to their instance UUIDs, or an empty dict if no devices found. - """ - - mgmt_ip_to_instance_id_map = {} - - try: - response = self.get_device_ids_from_site(site_id) - - if not response: - raise ValueError("No response received from get_device_ids_from_site") - - self.log("Received API response from 'get_device_ids_from_site': {0}".format(str(response)), "DEBUG") - - for device_id in response: - device_response = self.dnac._exec( - family="devices", - function="get_device_by_id", - op_modifies=True, - params={"id": device_id} - ) - - management_ip = device_response.get("response", {}).get("managementIpAddress") - instance_uuid = device_response.get("response", {}).get("instanceUuid") - if management_ip and instance_uuid: - mgmt_ip_to_instance_id_map[management_ip] = instance_uuid - else: - self.log("Management IP or instance UUID not found for device ID: {0}".format(device_id), "WARNING") - - except Exception as e: - self.log("Unable to fetch the device(s) associated with the site '{0}' due to {1}".format(site_id, str(e)), "ERROR") - return {} - - if not mgmt_ip_to_instance_id_map: - self.log("No reachable devices found at Site: {0}".format(site_id), "INFO") - - return mgmt_ip_to_instance_id_map - def get_sites_type(self, site_name): """ Get the type of a site in Cisco Catalyst Center. - Parameters: + Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. site_name (str): The name of the site for which to retrieve the type. Returns: @@ -647,7 +616,6 @@ def get_sites_type(self, site_name): get_site API with the provided site name, extracts the site type from the response, and returns it. If the specified site is not found, the function returns None, and an appropriate log message is generated. """ - try: response = self.get_site(site_name) if self.get_ccc_version_as_integer() <= self.get_ccc_version_as_int_from_str("2.3.5.3"): @@ -663,19 +631,16 @@ def get_sites_type(self, site_name): site_type = site[0].get("type") except Exception as e: - self.msg = "Error while fetching the site '{0}' and the specified site was not found in Cisco Catalyst Center.".format(site_name) - self.log(self.msg, "ERROR") - self.module.fail_json(msg=self.msg, response=[self.msg]) + 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): """ Retrieve device IDs associated with a specific site in Cisco Catalyst Center. - Args: site_id (str): The unique identifier of the site. - Returns: tuple: A tuple containing the API response and a list of device IDs associated with the site. Returns an empty list if no devices are found or if an error occurs. @@ -683,84 +648,126 @@ def get_device_ids_from_site(self, site_id): device_ids = [] api_response = None + 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: - try: - api_response = self.dnac._exec( - family="sites", - function="get_membership", - op_modifies=True, - params={"site_id": site_id}, - ) - - if api_response and "device" in api_response: - for device in api_response.get("device", []): - for item in device.get("response", []): - device_ids.append(item.get("instanceUuid")) + get_membership_params = {"site_id": site_id} + api_response = self.execute_get_request("sites", "get_membership", get_membership_params) - self.log("Retrieved device IDs from membership for site '{0}': {1}".format(site_id, device_ids), "DEBUG") - - except Exception as e: - self.log("Error retrieving device IDs from membership for site '{0}': {1}".format(site_id, str(e)), "ERROR") + if api_response and "device" in api_response: + for device in api_response.get("device", []): + for item in device.get("response", []): + device_ids.append(item.get("instanceUuid")) + self.log("Retrieved device IDs from membership for site '{0}': {1}".format(site_id, device_ids), "DEBUG") else: - try: - api_response = self.dnac._exec( - family="site_design", - function="get_site_assigned_network_devices", - op_modifies=True, - params={"site_id": site_id}, - ) + 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) - if api_response and "response" in api_response: - for device in api_response.get("response", []): - device_ids.append(device.get("deviceId")) + if api_response and "response" in api_response: + for device in api_response.get("response", []): + device_ids.append(device.get("deviceId")) - self.log("Retrieved device IDs from assigned devices for site '{0}': {1}".format(site_id, device_ids), "DEBUG") - - except Exception as e: - self.log("Error retrieving device IDs from assigned devices for site '{0}': {1}".format(site_id, str(e)), "ERROR") + 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") return api_response, device_ids - def get_site_id(self, site_name): + def get_device_details_from_site(self, site_id): """ - Retrieve the site ID and check if the site exists in Cisco Catalyst Center based on the provided site name. - + Retrieves device details for all devices within a specified site. Args: - - site_name (str): The name or hierarchy of the site to be retrieved. - + site_id (str): The ID of the site from which to retrieve device details. Returns: - - tuple (bool, str or None): A tuple containing: - 1. A boolean indicating whether the site exists (True if found, False otherwise). - 2. The site ID (str) if the site exists, or None if the site does not exist or an error occurs. - - Criteria: - - This function calls `get_site()` to retrieve site details from the Cisco Catalyst Center SDK. - - If the site exists, its ID is extracted from the response and returned. - - If the site does not exist or if an error occurs, an error message is logged, and the function returns a status of 'failed'. + list: A list of device details retrieved from the specified site. + Raises: + SystemExit: If the API call to get device IDs or device details fails. """ + device_details_list = [] + self.log("Initiating retrieval of device IDs for site ID: '{0}'.".format(site_id), "INFO") - try: - response = self.get_site(site_name) - if response is None: - raise ValueError - self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") - site = response.get("response") - site_id = site[0].get("id") - site_exists = True + # Retrieve device IDs from the specified site + api_response, device_ids = self.get_device_ids_from_site(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) - except Exception as e: - self.status = "failed" - self.msg = ("An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center.".format(site_name)) - self.result['response'] = self.msg - self.log(self.msg, "ERROR") - self.check_return_status() + self.log("Device IDs retrieved from site '{0}': {1}".format(site_id, str(device_ids)), "DEBUG") - return (site_exists, site_id) + # Iterate through each device ID to retrieve its details + for device_id in device_ids: + self.log("Initiating retrieval of device details for device ID: '{0}'.".format(device_id), "INFO") + + get_device_by_id_params = {"id": device_id} + + # Execute GET API call to retrieve device details + device_info = self.execute_get_request("devices", "get_device_by_id", get_device_by_id_params) + if not device_info: + self.msg = "No response received from API call 'get_device_by_id' for device ID: {0}".format(device_id) + self.fail_and_exit(self.msg) + + # Append the retrieved device details to the list + device_details_list.append(device_info.get("response")) + self.log("Device details retrieved for device ID: '{0}'.".format(device_id), "DEBUG") + + return device_details_list + + def get_reachable_devices_from_site(self, site_id): + """ + Retrieves a mapping of management IP addresses to instance IDs for reachable devices from a specified site. + Args: + site_id (str): The ID of the site from which to retrieve device details. + Returns: + tuple: A tuple containing: + - dict: A mapping of management IP addresses to instance IDs for reachable devices. + - list: A list of management IP addresses of skipped devices. + """ + mgmt_ip_to_instance_id_map = {} + skipped_devices_list = [] + + 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) + self.log("Device details retrieved for site ID: '{0}': {1}".format(site_id, device_details_list), "DEBUG") + + # Iterate through each device's details + for device_info in device_details_list: + management_ip = device_info.get("managementIpAddress") + instance_uuid = device_info.get("instanceUuid") + reachability_status = device_info.get("reachabilityStatus") + collection_status = device_info.get("collectionStatus") + device_family = device_info.get("family") + + # Check if the device is reachable and managed + if reachability_status == "Reachable" and collection_status == "Managed": + # Exclude Unified AP devices + if device_family != "Unified AP" : + mgmt_ip_to_instance_id_map[management_ip] = instance_uuid + else: + skipped_devices_list.append(management_ip) + msg = ( + "Skipping device {0} as its family is: {1}.".format( + management_ip, device_family + ) + ) + self.log(msg, "WARNING") + else: + skipped_devices_list.append(management_ip) + msg = ( + "Skipping device {0} as its status is {1} or its collectionStatus is {2}.".format( + management_ip, reachability_status, collection_status + ) + ) + self.log(msg, "WARNING") + + if not mgmt_ip_to_instance_id_map: + self.log("No reachable devices found at Site: {0}".format(site_id), "INFO") + + return mgmt_ip_to_instance_id_map, skipped_devices_list def get_site(self, site_name): """ @@ -775,60 +782,61 @@ def get_site(self, site_name): - If the response is empty, a warning is logged. - Any exceptions during the API call are caught, logged as errors, and the function returns None. """ + self.log("Initiating retrieval of site details for site name: '{0}'.".format(site_name), "DEBUG") + # Determine API call based on dnac_version if self.dnac_version <= self.version_2_3_5_3: - try: - response = self.dnac._exec( - family="sites", - function='get_site', - op_modifies=True, - params={"name": site_name}, - ) - - if not response: - self.log("The response from 'get_site' is empty.", "WARNING") - return None + get_site_params = {"name": site_name} + response = self.execute_get_request("sites", "get_site", get_site_params) + else: + get_sites_params = {"name_hierarchy": site_name} + response = self.execute_get_request("site_design", "get_sites", get_sites_params) - self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") - return response + return response - except Exception as e: - self.log("An error occurred in 'get_site':{0}".format(e), "ERROR") - return None + def get_site_id(self, site_name): + """ + Retrieve the site ID and check if the site exists in Cisco Catalyst Center based on the provided site name. + Args: + site_name (str): The name or hierarchy of the site to be retrieved. + Returns: + tuple (bool, str or None): A tuple containing: + 1. A boolean indicating whether the site exists (True if found, False otherwise). + 2. The site ID (str) if the site exists, or None if the site does not exist or an error occurs. + Criteria: + - This function calls `get_site()` to retrieve site details from the Cisco Catalyst Center SDK. + - If the site exists, its ID is extracted from the response and returned. + - If the site does not exist or if an error occurs, an error message is logged, and the function returns a status of 'failed'. + """ + try: + response = self.get_site(site_name) - else: - try: - response = self.dnac._exec( - family="site_design", - function='get_sites', - op_modifies=True, - params={"name_hierarchy": site_name}, - ) + # Check if the response is empty + if response is None: + self.msg = "No site details retrieved for site name: {0}".format(site_name) + self.fail_and_exit(self.msg) - if not response: - self.log("The response from 'get_sites' is empty.", "WARNING") - return None + self.log("Site details retrieved for site '{0}'': {1}".format(site_name, str(response)), "DEBUG") + site = response.get("response") + site_id = site[0].get("id") + site_exists = True - self.log("Received API response from 'get_sites': {0}".format(str(response)), "DEBUG") - return response + 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.fail_and_exit(self.msg) - except Exception as e: - self.log("An error occurred in 'get_sites':{0}".format(e), "ERROR") - return None + return (site_exists, site_id) def assign_device_to_site(self, device_ids, site_name, site_id): """ Assign devices to the specified site. - - Parameters: + Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_ids (list): A list of device IDs to assign to the specified site. site_name (str): The complete path of the site location. site_id (str): The ID of the specified site location. - Returns: bool: True if the devices are successfully assigned to the site, otherwise False. - Description: Assigns the specified devices to the site. If the assignment is successful, returns True. Otherwise, logs an error and returns False along with error details. @@ -952,10 +960,8 @@ def decrypt_password(self, encrypted_password, key): def camel_to_snake_case(self, config): """ Convert camel case keys to snake case keys in the config. - - Parameters: + Args: config (list) - Playbook details provided by the user. - Returns: new_config (list) - Updated config after eliminating the camel cases. """ @@ -977,10 +983,8 @@ def camel_to_snake_case(self, config): def update_site_type_key(self, config): """ Replace 'site_type' key with 'type' in the config. - - Parameters: + Args: config (list or dict) - Configuration details. - Returns: updated_config (list or dict) - Updated config after replacing the keys. """ @@ -1004,10 +1008,8 @@ def update_site_type_key(self, config): def is_valid_ipv4(self, ip_address): """ Validates an IPv4 address. - - Parameters: + Args: ip_address - String denoting the IPv4 address passed. - Returns: bool - Returns true if the passed IP address value is correct or it returns false if it is incorrect @@ -1028,10 +1030,8 @@ def is_valid_ipv4(self, ip_address): def is_valid_ipv6(self, ip_address): """ Validates an IPv6 address. - - Parameters: + Args: ip_address - String denoting the IPv6 address passed. - Returns: bool: True if the IPv6 address is valid, otherwise False """ @@ -1044,15 +1044,12 @@ def is_valid_ipv6(self, ip_address): def map_config_key_to_api_param(self, keymap=None, data=None): """ Converts keys in a dictionary from CamelCase to snake_case and creates a keymap. - - Parameters: + Args: keymap (dict): Already existing key map dictionary to add to or empty dict {}. data (dict or list): Input data where keys need to be mapped using the key map. - Returns: dict: A dictionary with the original keys as values and the converted snake_case keys as keys. - Example: functions = Accesspoint(module) keymap = functions.map_config_key_to_api_param(keymap, device_data) @@ -1085,10 +1082,8 @@ def map_config_key_to_api_param(self, keymap=None, data=None): def pprint(self, jsondata): """ Pretty prints JSON/dictionary data in a readable format. - - Parameters: + Args: jsondata (dict): Dictionary data to be printed. - Returns: str: Formatted JSON string. """ @@ -1173,10 +1168,8 @@ def is_valid_server_address(self, server_address): def is_path_exists(self, file_path): """ Check if the file path 'file_path' exists or not. - - Parameters: + Args: file_path (string) - Path of the provided file. - Returns: True/False (bool) - True if the file path exists, else False. """ @@ -1193,10 +1186,8 @@ def is_path_exists(self, file_path): def is_json(self, file_path): """ Check if the file in the file path is JSON or not. - - Parameters: + Args: file_path (string) - Path of the provided file. - Returns: True/False (bool) - True if the file is in JSON format, else False. """ @@ -1213,10 +1204,8 @@ def is_json(self, file_path): def check_task_tree_response(self, task_id): """ Returns the task tree response of the task ID. - - Parameters: + Args: task_id (string) - The unique identifier of the task for which you want to retrieve details. - Returns: error_msg (str) - Returns the task tree error message of the task ID. """ @@ -1226,8 +1215,8 @@ def check_task_tree_response(self, task_id): function='get_task_tree', params={"task_id": task_id} ) - self.log("Retrieving task tree details by the API 'get_task_tree' using task ID: {task_id}, Response: {response}" - .format(task_id=task_id, response=response), "DEBUG") + self.log("Retrieving task tree details by the API 'get_task_tree' using task ID: {0}, Response: {1}" + .format(task_id, response), "DEBUG") error_msg = "" if response and isinstance(response, dict): result = response.get('response') @@ -1244,7 +1233,6 @@ def check_task_tree_response(self, task_id): def get_task_details_by_id(self, task_id): """ Get the details of a specific task in Cisco Catalyst Center. - Args: self (object): An instance of a class that provides access to Cisco Catalyst Center. task_id (str): The unique identifier of the task for which you want to retrieve details. @@ -1282,7 +1270,6 @@ def get_task_details_by_id(self, task_id): def get_tasks_by_id(self, task_id): """ Get the tasks of a task ID in Cisco Catalyst Center. - Args: self (object): An instance of a class that provides access to Cisco Catalyst Center. task_id (str): The unique identifier of the task for which you want to retrieve details. @@ -1301,16 +1288,16 @@ def get_tasks_by_id(self, task_id): function="get_tasks_by_id", params={"id": task_id} ) - self.log('Task Details: {response}'.format(response=response), 'DEBUG') - self.log("Retrieving task details by the API 'get_tasks_by_id' using task ID: {task_id}, Response: {response}" - .format(task_id=task_id, response=response), "DEBUG") + self.log('Task Details: {0}'.format(response), 'DEBUG') + self.log("Retrieving task details by the API 'get_tasks_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("Task Status: {0}".format(task_status), "DEBUG") except Exception as e: # Log an error message and fail if an exception occurs self.log_traceback() @@ -1324,8 +1311,7 @@ def get_tasks_by_id(self, task_id): def check_tasks_response_status(self, response, api_name): """ Get the task response status from taskId - - Parameters: + Args: self: The current object details. response (dict): API response. api_name (str): API name. @@ -1358,16 +1344,16 @@ def check_tasks_response_status(self, response, api_name): while True: elapsed_time = time.time() - start_time if elapsed_time >= self.max_timeout: - self.msg = "Max timeout of {max_timeout} sec has reached for the task id '{task_id}'. " \ - .format(max_timeout=self.max_timeout, task_id=task_id) + \ - "Exiting the loop due to unexpected API '{api_name}' status.".format(api_name=api_name) + self.msg = "Max timeout of {0} sec has reached for the task id '{1}'. " \ + .format(self.max_timeout, task_id) + \ + "Exiting the loop due to unexpected API '{0}' status.".format(api_name) self.log(self.msg, "WARNING") self.status = "failed" break task_details = self.get_tasks_by_id(task_id) - self.log('Getting tasks details from task ID {task_id}: {task_details}' - .format(task_id=task_id, task_details=task_details), "DEBUG") + self.log('Getting tasks details from task ID {0}: {1}' + .format(task_id, task_details), "DEBUG") task_status = task_details.get("status") if task_status == "FAILURE": @@ -1378,19 +1364,19 @@ def check_tasks_response_status(self, response, api_name): elif task_status == "SUCCESS": self.result["changed"] = True - self.log("The task with task ID '{task_id}' is executed successfully." - .format(task_id=task_id), "INFO") + self.log("The task with task ID '{0}' is executed successfully." + .format(task_id), "INFO") break - self.log("Progress is {status} for task ID: {task_id}" - .format(status=task_status, task_id=task_id), "DEBUG") + self.log("Progress is {0} for task ID: {1}" + .format(task_status, task_id), "DEBUG") return self def set_operation_result(self, operation_status, is_changed, status_message, log_level, additional_info=None): """ Update the result of the operation with the provided status, message, and log level. - Parameters: + Args: - operation_status (str): The status of the operation ("success" or "failed"). - is_changed (bool): Indicates whether the operation caused changes. - status_message (str): The message describing the result of the operation. @@ -1436,7 +1422,7 @@ def log_traceback(self): def check_timeout_and_exit(self, loop_start_time, task_id, task_name): """ Check if the elapsed time exceeds the specified timeout period and exit the while loop if it does. - Parameters: + Args: - loop_start_time (float): The time when the while loop started. - task_id (str): ID of the task being monitored. - task_name (str): Name of the task being monitored. @@ -1455,12 +1441,74 @@ def check_timeout_and_exit(self, loop_start_time, task_id, task_name): return False - def get_taskid_post_api_call(self, api_family, api_function, api_parameters): + def execute_get_request(self, api_family, api_function, api_parameters): + """ + Makes a GET API call to the specified function within a given family and returns the response. + Args: + api_family (str): The family of the API to call. + api_function (str): The specific function of the API to call. + api_parameters (dict): Parameters to pass to the API call. + Returns: + dict or None: The response from the API call if successful, otherwise None. + Logs detailed information about the API call, including responses and errors. """ - Executes the specified API call with given parameters and logs responses. + self.log( + "Initiating GET API call for Function: {0} from family: {1} with Parameters: {2}.".format( + api_function, api_family, api_parameters + ), + "DEBUG" + ) + try: + # Execute the API call + response = self.dnac._exec( + family=api_family, + function=api_function, + op_modifies=False, + params=api_parameters, + ) + + # Log the response received + self.log( + "Response received from GET API call to Function: '{0}' from Family: '{1}' is Response: {2}".format( + api_function, api_family, str(response) + ), + "INFO" + ) + + # Check if the response is None, an empty string, or an empty dictionary + if response is None or response == "" or (isinstance(response, dict) and not response): + self.log( + "No response received from GET API call to Function: '{0}' from Family: '{1}'.".format( + api_function, api_family + ), "WARNING" + ) + return None - Parameters: - api_family (str): The API family (e.g., "sda"). + # Check if the 'response' key is present and empty + if isinstance(response, dict) and 'response' in response and not response['response']: + self.log( + "Empty 'response' key in the API response from GET API call to Function: '{0}' from Family: '{1}'.".format( + api_function, api_family + ), "WARNING" + ) + return None + + return response + + except Exception as e: + # Log an error message and fail if an exception occurs + self.log_traceback() + self.msg = ( + "An error occurred while executing GET API call to Function: '{0}' from Family: '{1}'. " + "Parameters: {2}. Exception: {3}.".format(api_function, api_family, api_parameters, str(e)) + ) + self.fail_and_exit(self.msg) + + def get_taskid_post_api_call(self, api_family, api_function, api_parameters): + """ + Retrieve task ID from response after executing POST API call + Args: + api_family (str): The API family. api_function (str): The API function (e.g., "add_port_assignments"). api_parameters (dict): The parameters for the API call. """ @@ -1515,18 +1563,15 @@ def get_taskid_post_api_call(self, api_family, api_function, api_parameters): def get_task_status_from_tasks_by_id(self, task_id, task_name, success_msg): """ Retrieves and monitors the status of a task by its task ID. - This function continuously checks the status of a specified task using its task ID. If the task completes successfully, it updates the message and status accordingly. If the task fails or times out, it handles the error and updates the status and message. - - Parameters: - - task_id (str): The unique identifier of the task to monitor. - - task_name (str): The name of the task being monitored. - - success_msg (str): The success message to set if the task completes successfully. - + Args: + task_id (str): The unique identifier of the task to monitor. + task_name (str): The name of the task being monitored. + success_msg (str): The success message to set if the task completes successfully. Returns: - - self: The instance of the class with updated status and message. + self: The instance of the class with updated status and message. """ loop_start_time = time.time() @@ -1567,7 +1612,70 @@ def get_task_status_from_tasks_by_id(self, task_id, task_name, success_msg): self.set_operation_result("success", True, self.msg, "INFO") break + # Wait for the specified poll interval before the next check time.sleep(self.params.get("dnac_task_poll_interval")) + + 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): + """ + Retrieves and monitors the status of a task by its ID and validates the task's data or progress. + Args: + task_id (str): The ID of the task to check. + task_name (str): The name of the task. + data_validation (str, optional): A key to validate the task's data. Defaults to None. + progress_validation (str, optional): A key to validate the task's progress. Defaults to None. + failure_msg (str, optional): A custom message to log if the task fails. Defaults to None. + success_msg (str, optional): A custom message to log if the task succeeds. Defaults to None. + Returns: + self: The instance of the class. + """ + loop_start_time = time.time() + 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.set_operation_result("failed", False, self.msg, "ERROR") + break + + # Check if there is an error in the task response + if response.get("isError"): + failure_reason = response.get("failureReason") + self.msg = failure_reason + if failure_reason: + self.msg += "Failure reason: {0}".format(failure_reason) + self.set_operation_result("failed", False, self.msg, "ERROR") + break + + # Check if the elapsed time exceeds the timeout + if self.check_timeout_and_exit(loop_start_time, task_id, task_name): + break + + # Extract data, progress, and end time from the response + data = response.get("data") + progress = response.get("progress") + end_time = response.get("endTime") + + # Validate task data if data_validation key is provided + if data_validation: + if end_time and data_validation in data: + self.msg = success_msg + self.set_operation_result("success", True, self.msg, "INFO") + break + + # Validate task progress if progress_validation key is provided + if progress_validation: + if end_time and progress_validation in progress: + self.msg = success_msg + self.set_operation_result("success", True, self.msg, "INFO") + break + + # Wait for the specified poll interval before the next check + time.sleep(self.params.get("dnac_task_poll_interval")) + return self def requires_update(self, have, want, obj_params): @@ -1579,7 +1687,7 @@ def requires_update(self, have, want, obj_params): Cisco Catalyst Center with the user-provided details from the playbook, using a specified schema for comparison. - Parameters: + Args: have (dict): Current information from the Cisco Catalyst Center of SDA fabric devices. want (dict): Users provided information from the playbook diff --git a/plugins/modules/device_configs_backup_workflow_manager.py b/plugins/modules/device_configs_backup_workflow_manager.py index 3011ffc3b2..9747ec4c11 100644 --- a/plugins/modules/device_configs_backup_workflow_manager.py +++ b/plugins/modules/device_configs_backup_workflow_manager.py @@ -617,7 +617,7 @@ def get_device_id_list(self, config): 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 = self.get_device_ip_from_device_id(site_id) + 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) @@ -802,34 +802,6 @@ def export_device_configurations(self, export_device_configurations_params): self.set_operation_result("failed", False, self.msg, "ERROR") self.check_return_status() - def exit_while_loop(self, start_time, task_id, task_name, response): - """ - Check if the elapsed time exceeds the specified timeout period and exit the while loop if it does. - Parameters: - - start_time (float): The time when the while loop started. - - task_id (str): ID of the task being monitored. - - task_name (str): Name of the task being monitored. - - response (dict): Response received from the task status check. - Returns: - bool: True if the elapsed time exceeds the timeout period, False otherwise. - """ - # If the elapsed time exceeds the timeout period - if time.time() - start_time > self.params.get("dnac_api_task_timeout"): - if response.get("data"): - # If there is data in the response, include it in the error message - self.msg = "Task {0} with task id {1} has not completed within the timeout period. Task Status: {2} ".format( - task_name, task_id, response.get("data")) - else: - # If there is no data in the response, generate a generic error message - self.msg = "Task {0} with task id {1} has not completed within the timeout period.".format( - task_name, task_id) - - # Update the result with failure status and log the error message - self.update_result("failed", False, self.msg, "ERROR") - return True - - return False - def download_file(self, additional_status_url=None): """ Downloads a file from Cisco Catalyst Center and stores it locally. @@ -916,71 +888,55 @@ def get_export_device_config_task_status(self, task_id): file if the task completes successfully. """ task_name = "Backup Device Configuration" - start_time = time.time() - - while True: - # Retrieve the task status using the task ID - response = self.get_task_details(task_id) + success_msg = "{0} Task with task ID {1} completed successfully. Exiting the loop.".format(task_name, task_id) + if self.dnac_version <= self.version_2_3_5_3: + progress_validation = "Device configuration Successfully exported as password protected ZIP" + failure_msg = ( + "An error occurred while performing {0} task with task ID {1} for export_device_configurations_params: {2}" + .format(task_name, task_id, self.want.get("export_device_configurations_params")) + ) + self.get_task_status_from_task_by_id(task_id, task_name, failure_msg, success_msg, progress_validation=progress_validation) + else: + self.get_task_status_from_tasks_by_id(task_id, task_name, success_msg) - # Check if response returned - if not response: - self.msg = "Error retrieving Task status for the task_name {0} task_id {1}".format(task_name, task_id) - self.set_operation_result("failed", False, self.msg, "ERROR") - return self - - # Check if the elapsed time exceeds the timeout - if self.exit_while_loop(start_time, task_id, task_name, response): - return self - - # Handle error if task execution encounters an error - if response.get("isError") or re.search("failed", response.get("progress"), flags=re.IGNORECASE): - failure_reason = response.get("failureReason", "No detailed reason provided") + if self.status == "success": + if self.dnac_version <= self.version_2_3_5_3: + response = self.get_task_details(task_id) + additional_status_url = response.get("additionalStatusURL") + else: + response = self.get_tasks_by_id(task_id) + additional_status_url = response.get("resultLocation") + + if not additional_status_url: + self.msg = "Error retrieving the Device Config Backup file ID for task ID {0}".format(task_id) + fail_and_exit(self.msg) + + # 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 + 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: + self.msg = "Error downloading Device Config Backup file(s) with file ID: {0}. ".format(file_id) + self.fail_and_exit(self.msg) + + # Unzip the downloaded file + 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( + task_name, len(mgmt_ip_to_instance_id_map), list(mgmt_ip_to_instance_id_map.keys())), "INFO") + self.log("{0} task has been skipped for {1} device(s): {2}".format( + task_name, len(self.skipped_devices_list), self.skipped_devices_list), "INFO") self.msg = ( - "An error occurred while performing {0} task for export_device_configurations_params: {1}. " - "The operation failed due to the following reason: {2}".format( - task_name, self.want.get("export_device_configurations_params"), failure_reason - ) + "{0} task has been successfully performed on {1} device(s) and skipped on {2} device(s). " + "The backup configuration files can be found at: {3}".format( + task_name, len(mgmt_ip_to_instance_id_map), len(self.skipped_devices_list), pathlib.Path(self.want.get("file_path")).resolve()) ) - self.set_operation_result("failed", False, self.msg, "ERROR") - return self - - # Check if task completed successfully and exit the loop - if not response.get("isError") and response.get("progress") == "Device configuration Successfully exported as password protected ZIP.": - self.log("{0} Task completed successfully. Exiting the loop.".format(task_name), "INFO") - break - - self.log("The progress status is {0}, continue to check the status after 3 seconds. Putting into sleep for 3 seconds.".format( - response.get("progress")), "INFO") - time.sleep(3) - - # Perform additional tasks after breaking the loop - mgmt_ip_to_instance_id_map = self.want.get("mgmt_ip_to_instance_id_map") - additional_status_url = response.get("additionalStatusURL") - - # Download the file using the additional status URL - 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: - self.msg = "Error downloading Device Config Backup file(s) with file ID: {0}. ".format(file_id) - self.set_operation_result("Failed", True, self.msg, "CRITICAL") - return self - - # Unzip the downloaded file - 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( - task_name, len(mgmt_ip_to_instance_id_map), list(mgmt_ip_to_instance_id_map.keys())), "INFO") - self.log("{0} task has been skipped for {1} device(s): {2}".format( - task_name, len(self.skipped_devices_list), self.skipped_devices_list), "INFO") - self.msg = ( - "{0} task has been successfully performed on {1} device(s) and skipped on {2} device(s). " - "The backup configuration files can be found at: {3}".format( - task_name, len(mgmt_ip_to_instance_id_map), len(self.skipped_devices_list), pathlib.Path(self.want.get("file_path")).resolve()) - ) - self.set_operation_result("success", True, self.msg, "INFO") - else: - self.msg = "Error unzipping Device Config Backup file(s) with file ID: {0}. ".format(file_id) - self.set_operation_result("failed", False, self.msg, "ERROR") + self.set_operation_result("success", True, self.msg, "INFO") + else: + self.msg = "Error unzipping Device Config Backup file(s) with file ID: {0}. ".format(file_id) + self.fail_and_exit(self.msg) return self diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 7f0fc66962..540e9a4f2a 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -98,7 +98,6 @@ compliance.Compliance.commit_device_configuration task.Task.get_task_by_id task.Task.get_task_details_by_id - task.Task.get_task_tree task.Task.get_tasks compliance.Compliance.compliance_details_of_device devices.Devices.get_device_list @@ -112,7 +111,6 @@ post /dna/intent/api/v1/compliance/ post /dna/intent/api/v1/network-device-config/write-memory get /dna/intent/api/v1/task/{taskId} - get /dna/intent/api/v1/task/{taskId}/tree get /dna/intent/api/v1/compliance/${deviceUuid}/detail get /dna/intent/api/v1/membership/${siteId} get /dna/intent/api/v1/site @@ -351,7 +349,6 @@ } """ -import time from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -365,18 +362,20 @@ class NetworkCompliance(DnacBase): def __init__(self, module): """ Initialize an instance of the class. - Parameters: + Args: - module: The module associated with the class instance. Returns: The method does not return a value. """ super().__init__(module) + self.skipped_run_compliance_devices_list = [] + self.skipped_sync_device_configs_list = [] def validate_input(self): """ Validate the fields provided in the playbook against a predefined specification to ensure they adhere to the expected structure and data types. - Parameters: + Args: state (optional): A state parameter that can be used to customize validation based on different conditions. Returns: @@ -428,13 +427,15 @@ def validate_input(self): def validate_ip4_address_list(self, ip_address_list): """ Validates the list of IPv4 addresses provided in the playbook. - Parameters: + Args: ip_address_list (list): A list of IPv4 addresses to be validated. Description: This method iterates through each IP address in the list and checks if it is a valid IPv4 address. If any address is found to be invalid, it logs an error message and fails. After validating all IP addresses, it logs a success message. """ + self.log("Validating the IP addresses in the ip_address_list: {0}".format(ip_address_list), "DEBUG") + for ip in ip_address_list: if not self.is_valid_ipv4(ip): self.msg = "IP address: {0} is not valid".format(ip) @@ -444,10 +445,77 @@ def validate_ip4_address_list(self, ip_address_list): ip_address_list_str = ", ".join(ip_address_list) self.log("Successfully validated the IP address(es): {0}".format(ip_address_list_str), "DEBUG") - def validate_run_compliance_paramters(self, mgmt_ip_to_instance_id_map, run_compliance, run_compliance_categories): + def validate_iplist_and_site_name(self, ip_address_list, site_name): + """ + Validates that either an IP address list or a site name is provided. + This function checks if at least one of the parameters `ip_address_list` or `site_name` is provided. + If neither is provided, it logs an error message and exits the process. If validation is successful, + it logs a success message. + Args: + ip_address_list (list): A list of IP addresses to be validated. + site_name (str): A site name to be validated. + Raises: + SystemExit: If neither `ip_address_list` nor `site_name` is provided, the function logs an error message and exits the process. + """ + self.log("Validating 'ip_address_list': '{0}' or 'site_name': '{1}'".format(ip_address_list, site_name), "DEBUG") + + # 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( + ip_address_list, site_name) + self.fail_and_exit(self.msg) + + # Validate if valid ip_addresses in the ip_address_list + if ip_address_list: + self.validate_ip4_address_list(ip_address_list) + + 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") + + 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.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") + + valid_categories = ["INTENT", "RUNNING_CONFIG", "IMAGE", "PSIRT", "EOX", "NETWORK_SETTINGS"] + if not all(category.upper() in valid_categories for category in run_compliance_categories): + valid_categories_str = ", ".join(valid_categories) + self.msg = "Invalid category provided. Valid categories are {0}.".format(valid_categories_str) + self.fail_and_exit(self.msg) + + 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") + 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") + + # Validate either ip_address_list OR site_name is present + self.validate_iplist_and_site_name(ip_address_list, site_name) + + # Validate if a network compliance operation is present + 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) + + self.log("Validation completed for configuration: {0}".format(config), "INFO") + + def get_run_compliance_params(self, mgmt_ip_to_instance_id_map, run_compliance, run_compliance_categories): """ Validate and prepare parameters for running compliance checks. - Parameters: + Args: - mgmt_ip_to_instance_id_map (dict): A dictionary mapping management IP addresses to device instance IDs. - run_compliance (bool or None): A boolean indicating whether to run compliance checks. - run_compliance_categories (list): A list of compliance categories to check. @@ -464,26 +532,13 @@ def validate_run_compliance_paramters(self, mgmt_ip_to_instance_id_map, run_comp """ # Initializing empty dicts/lists run_compliance_params = {} - valid_categories = ["INTENT", "RUNNING_CONFIG", "IMAGE", "PSIRT", "EOX", "NETWORK_SETTINGS"] - - if run_compliance_categories: - # Validate the categories provided - if not all(category.upper() in valid_categories for category in run_compliance_categories): - valid_categories_str = ", ".join(valid_categories) - msg = "Invalid category provided. Valid categories are {0}.".format(valid_categories_str) - self.log(msg, "ERROR") - self.module.fail_json(msg) - - if run_compliance: - # run_compliance_params - run_compliance_params["deviceUuids"] = list(mgmt_ip_to_instance_id_map.values()) - run_compliance_params["triggerFull"] = False - run_compliance_params["categories"] = run_compliance_categories + # Create run_compliance_params if run_compliance: - # run_compliance_params run_compliance_params["deviceUuids"] = list(mgmt_ip_to_instance_id_map.values()) - run_compliance_params["triggerFull"] = True + run_compliance_params["triggerFull"] = not bool(run_compliance_categories) + if run_compliance_categories: + run_compliance_params["categories"] = run_compliance_categories # Check for devices with Compliance Status of "IN_PROGRESS" and update parameters accordingly if run_compliance_params: @@ -493,13 +548,12 @@ def validate_run_compliance_paramters(self, mgmt_ip_to_instance_id_map, run_comp if not response: ip_address_list_str = ", ".join(list(mgmt_ip_to_instance_id_map.keys())) - msg = ( + self.msg = ( "Error occurred when retrieving Compliance Report to identify if there are " "devices with 'IN_PROGRESS' status. This is required on device(s): {0}" .format(ip_address_list_str) ) - self.log(msg) - self.module.fail_json(msg) + self.fail_and_exit(self.msg) # Iterate through the response to identify devices with 'IN_PROGRESS' status for device_ip, compliance_details_list in response.items(): @@ -517,62 +571,145 @@ def validate_run_compliance_paramters(self, mgmt_ip_to_instance_id_map, run_comp msg = "Excluding 'IN_PROGRESS' devices from compliance check. Updated run_compliance_params: {0}".format(run_compliance_params) self.log(msg, "DEBUG") + self.log("run_compliance_params: {0}".format(run_compliance_params), "DEBUG") return run_compliance_params - def get_device_ids_from_ip(self, ip_address_list): + def get_sync_device_config_params(self, mgmt_ip_to_instance_id_map, categorized_devices): + sync_device_config_params = { + "deviceId": list(mgmt_ip_to_instance_id_map.values()) + } + + other_device_ips = categorized_devices.get("OTHER", {}).keys() + compliant_device_ips = categorized_devices.get("COMPLIANT", {}).keys() + excluded_device_ips = set(other_device_ips) | set(compliant_device_ips) + + 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] + sync_device_config_params["deviceId"] = [ + device_id for device_id in mgmt_ip_to_instance_id_map.values() + if device_id not in excluded_device_uuids + ] + excluded_device_ips_str = ", ".join(excluded_device_ips) + msg = "Skipping these devices because their compliance status is not 'NON_COMPLIANT': {0}".format(excluded_device_ips_str) + 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") + return sync_device_config_params + + def get_device_list_params(self, ip_address_list): """ - Retrieves the device IDs based on the provided list of IP addresses from Cisco Catalyst Center. - Parameters: - ip_address_list (list): A list of IP addresses of devices for which you want to retrieve the device IDs. + Generates a dictionary of device parameters for querying Cisco Catalyst Center. + Args: + config (dict): A dictionary containing device filter criteria. + Returns: + dict: A dictionary mapping internal parameter names to their corresponding values from the config. + Description: + This method takes a configuration dictionary containing various device filter criteria and maps them to the internal parameter + 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. + """ + # 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") + return get_device_list_params + + def get_device_ids_from_ip(self, get_device_list_params): + """Retrieves device IDs based on specified parameters from Cisco Catalyst Center. + Args: + get_device_list_params (dict): A dictionary of parameters to filter devices. Returns: - dict: A dictionary mapping management IP addresses to their instance UUIDs. + dict: A dictionary mapping management IP addresses to instance IDs of reachable devices that are not Unified APs. Description: - This method queries Cisco Catalyst Center for device information using the provided IP addresses. - For each IP address in the list, it attempts to fetch the device information using the "get_device_list" API. - If the device is found and reachable, it extracts the device ID and maps it to the corresponding IP address. - If any error occurs during the process, it logs an error message and continues to the next IP address. + This method queries Cisco Catalyst Center to retrieve device information based on the provided filter parameters. + It paginates through the results, filters out unreachable devices and Unified APs, and returns a dictionary of management IP addresses + 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. """ mgmt_ip_to_instance_id_map = {} + processed_device_count = 0 + skipped_device_count = 0 - for device_ip in ip_address_list: - try: - # Query Cisco Catalyst Center for device information using the IP address + try: + offset = 1 + limit = 500 + while True: + # Update params with current offset and limit + get_device_list_params.update({ + "offset": offset, + "limit": limit + }) + + # Query Cisco Catalyst Center for device information using the parameters response = self.dnac._exec( family="devices", function="get_device_list", op_modifies=True, - params={"managementIpAddress": device_ip} + params=get_device_list_params ) - self.log("Response received post 'get_device_list' API call: {0}".format(str(response)), "DEBUG") + self.log("Response received post 'get_device_list' API call with offset {0}: {1}".format(offset, str(response)), "DEBUG") # Check if a valid response is received - if response.get("response"): - response = response.get("response") - if not response: - continue - for device_info in response: - if device_info["reachabilityStatus"] == "Reachable": - if device_info["family"] != "Unified AP": - device_id = device_info["id"] - mgmt_ip_to_instance_id_map[device_ip] = device_id - else: - msg = "Skipping device {0} as its family is {1}.".format(device_ip, device_info["family"]) - self.log(msg, "INFO") + if not response.get("response"): + self.log("Exiting the loop because no devices were returned after increasing the offset. Current offset: {0}".format(offset)) + break # Exit loop if no devices are returned + + # Iterate over the devices in the response + for device_info in response.get("response", []): + processed_device_count += 1 + device_ip = device_info.get("managementIpAddress", "Unknown IP") + + # Check if the device is reachable and managed + if device_info.get("reachabilityStatus") == "Reachable" and device_info.get("collectionStatus") == "Managed": + # Skip Unified AP devices + if device_info.get("family") != "Unified AP" : + device_id = device_info["id"] + mgmt_ip_to_instance_id_map[device_ip] = device_id else: - msg = "Skipping device {0} as its status is {2}.".format(device_ip, device_info["reachabilityStatus"]) + skipped_device_count += 1 + self.skipped_run_compliance_devices_list.append(device_ip) + 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") + ) + ) self.log(msg, "INFO") - else: - # If unable to retrieve device information, log an error message - self.log("Unable to retrieve device information for {0}. Please ensure that the device exists and is reachable.".format(device_ip), "ERROR") - except Exception as e: - # Log an error message if any exception occurs during the process - self.log("Error while fetching device ID for device: '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)), "ERROR") + else: + self.skipped_run_compliance_devices_list.append(device_ip) + self.skipped_sync_device_configs_list.append(device_ip) + 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") + ) + ) + self.log(msg, "INFO") + + # Increment offset for next batch + offset += limit + + # Check if the IP from get_device_list_params is in mgmt_ip_to_instance_id_map + for ip in get_device_list_params.get("management_ip_address", []): + if ip not in mgmt_ip_to_instance_id_map: + self.skipped_run_compliance_devices_list.append(ip) + self.skipped_sync_device_configs_list.append(ip) + + # Log the total number of devices processed and skipped + self.log("Total number of devices received: {0}".format(processed_device_count), "INFO") + self.log("Number of devices that are Unreachable or APs: {0}".format(skipped_device_count), "INFO") + self.log("Config Backup Operation can be performed on the following filtered devices: {0}".format(len(mgmt_ip_to_instance_id_map)), "INFO") + + except Exception as e: + # Log an error message if any exception occurs during the process + self.log("Error fetching device IDs from Cisco Catalyst Center. Error details: {0}".format(str(e)), "ERROR") + + # Log an error if no reachable devices are found if not mgmt_ip_to_instance_id_map: - ip_address_list_str = ", ".join(ip_address_list) - self.msg = "No reachable devices found among the provided IP addresses: {0}".format(ip_address_list_str) - self.set_operation_result("ok", False, self.msg, "INFO") - self.module.exit_json(**self.result) + self.log("No reachable devices found among the provided parameters: {0}".format(mgmt_ip_to_instance_id_map), "ERROR") return mgmt_ip_to_instance_id_map @@ -580,7 +717,7 @@ def get_device_id_list(self, ip_address_list, site_name): """ Get the list of unique device IDs for a specified list of management IP addresses or devices associated with a site in Cisco Catalyst Center. - Parameters: + Args: ip_address_list (list): The management IP addresses of devices for which you want to retrieve the device IDs. site_name (str): The name of the site for which you want to retrieve the device IDs. Returns: @@ -592,18 +729,28 @@ def get_device_id_list(self, ip_address_list, site_name): # 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 + 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 = self.get_device_ip_from_device_id(site_id) + 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) - if ip_address_list: - # Retrieve device IDs associated with devices having specified IP addresses - iplist_mgmt_ip_to_instance_id_map = self.get_device_ids_from_ip(ip_address_list) - mgmt_ip_to_instance_id_map.update(iplist_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 + ip_address_list_str = ", ".join(ip_address_list) + self.msg = ("No device UUIDs were fetched for network compliance operations with the provided IP address(es): {0} " + "or site name: {1}. This could be due to Unreachable devices or access points (APs).").format(ip_address_list_str, site_name) + self.fail_and_exit(self.msg) return mgmt_ip_to_instance_id_map @@ -656,137 +803,35 @@ def is_sync_required(self, response, mgmt_ip_to_instance_id_map): return required, msg, categorized_devices - def get_want(self, config): + def get_compliance_details_of_device(self, compliance_details_of_device_params, device_ip): """ - Determines the desired state based on the provided configuration. - Parameters: - config (dict): The configuration specifying the desired state. + Retrieve compliance details for a specific device. + This function makes an API call to fetch compliance details for a given device + using the specified parameters. It handles the API response and logs the + necessary information. + Args: + compliance_details_of_device_params (dict): Parameters required for the compliance details API call. + device_ip (str): The IP address of the device for which compliance details are being fetched. Returns: - dict: A dictionary containing the desired state parameters. - Description: - This method processes the provided configuration to determine the desired state. It validates the presence of - either "ip_address_list" or "site_name" and constructs parameters for running compliance checks and syncing - device configurations based on the provided configuration. It also logs the desired state for reference. + dict or None: The response from the compliance details API call if successful, + None if an error occurs or no response is received. """ - # Initialize parameters - run_compliance_params = {} - sync_device_config_params = {} - compliance_detail_params_sync = {} - compliance_details = {} - - # Store input parameters - 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") - run_compliance_batch_size = config.get("run_compliance_batch_size") - - # Validate either ip_address_list OR site_name is present - if not any([ip_address_list, site_name]): - msg = "ip_address_list is {0} and site_name is {1}. Either the ip_address_list or the site_name must be provided.".format( - ip_address_list, site_name) - self.log(msg, "ERROR") - self.module.fail_json(msg=msg) - - # Validate if a network compliance operation is present - if not any([run_compliance, run_compliance_categories, sync_device_config]): - msg = "No actions were requested. This network compliance module can perform the following tasks: Run Compliance Check or Sync Device Config." - self.log(msg, "ERROR") - self.module.fail_json(msg) - return self - - # Validate valid ip_addresses - if ip_address_list: - self.validate_ip4_address_list(ip_address_list) - # Remove Duplicates from list - ip_address_list = list(set(ip_address_list)) - - # Retrieve device ID list - mgmt_ip_to_instance_id_map = self.get_device_id_list(ip_address_list, site_name) - if not mgmt_ip_to_instance_id_map: - # Log an error message if mgmt_ip_to_instance_id_map is empty - ip_address_list_str = ", ".join(ip_address_list) - msg = ("No device UUIDs were fetched for network compliance operations with the provided IP address(es): {0} " - "or site name: {1}. This could be due to Unreachable devices or access points (APs).").format(ip_address_list_str, site_name) - self.log(msg, "ERROR") - self.module.fail_json(msg) - - # Run Compliance Paramters - run_compliance_params = self.validate_run_compliance_paramters( - mgmt_ip_to_instance_id_map, run_compliance, run_compliance_categories) - - # Sync Device Configuration Parameters - if sync_device_config: - sync_device_config_params = { - "deviceId": list(mgmt_ip_to_instance_id_map.values()) - } - - compliance_detail_params_sync = { - "deviceUuids": list(mgmt_ip_to_instance_id_map.values()), - "categories": ["RUNNING_CONFIG"] - } - - # Validate if Sync Device Configuration is required on the device(s) - response = self.get_compliance_report(compliance_detail_params_sync, mgmt_ip_to_instance_id_map) - if not response: - ip_address_list_str = ", ".join(list(mgmt_ip_to_instance_id_map.keys())) - msg = "Error occurred when retrieving Compliance Report to identify if Sync Device Config Operation " - msg += "is required on device(s): {0}".format(ip_address_list_str) - self.log(msg) - self.module.fail_json(msg) - - compliance_details = response - sync_required, self.msg, categorized_devices = self.is_sync_required(response, mgmt_ip_to_instance_id_map) - self.log("Is Sync Requied: {0} -- Message: {1}".format(sync_required, self.msg), "DEBUG") - if not sync_required: - self.set_operation_result("ok", False, self.msg, "INFO") - self.module.exit_json(**self.result) - - # Get the device IDs of devices in the "OTHER" category and "COMPLIANT" category - other_device_ids = categorized_devices.get("OTHER", {}).keys() - compliant_device_ids = categorized_devices.get("COMPLIANT", {}).keys() - excluded_device_ids = set(other_device_ids) | set(compliant_device_ids) - - if excluded_device_ids: - # Convert excluded_device_ids to their corresponding UUIDs - excluded_device_uuids = [mgmt_ip_to_instance_id_map[ip] for ip in excluded_device_ids if ip in mgmt_ip_to_instance_id_map] - - # Exclude devices in the "OTHER" category from sync_device_config_params - sync_device_config_params["deviceId"] = [ - device_id for device_id in mgmt_ip_to_instance_id_map.values() - if device_id not in excluded_device_uuids - ] - excluded_device_ids_str = ", ".join(excluded_device_ids) - msg = "Skipping these devices because their compliance status is not 'NON_COMPLIANT': {0}".format(excluded_device_ids_str) - self.log(msg, "WARNING") - self.log("Updated 'sync_device_config_params' parameters: {0}".format(sync_device_config_params), "DEBUG") - - # Construct the "want" dictionary containing the desired state parameters - want = {} - want = dict( - ip_address_list=ip_address_list, - site_name=site_name, - mgmt_ip_to_instance_id_map=mgmt_ip_to_instance_id_map, - run_compliance_params=run_compliance_params, - run_compliance_batch_size=run_compliance_batch_size, - sync_device_config_params=sync_device_config_params, - compliance_detail_params_sync=compliance_detail_params_sync, - compliance_details=compliance_details - ) - self.want = want - self.log("Desired State (want): {0}".format(str(self.want)), "INFO") - - return self + self.log("Attempting to retrieve Compliance details for device: '{0}'".format(device_ip), "INFO") + response = self.execute_get_request("compliance", "compliance_details_of_device", compliance_details_of_device_params) + if response: + self.log("Sucessfully retrieved Compliance details for device: '{0}'".format(device_ip), "INFO") + return response.get("response") + else: + return None + # self.msg = "An error occurred while retrieving Compliance Details for device: '{0}'".format(device_ip) + # self.fail_and_exit(self.msg) def get_compliance_report(self, run_compliance_params, mgmt_ip_to_instance_id_map): """ Generate a compliance report for devices based on provided parameters. - This function fetches the compliance details for a list of devices specified in the run_compliance_params. It maps the device UUIDs to their corresponding management IPs, and then retrieves the compliance details for each device. - Args: run_compliance_params (dict): Parameters for running compliance checks. Expected to contain "deviceUuids" and optionally "categories". @@ -829,66 +874,29 @@ def get_compliance_report(self, run_compliance_params, mgmt_ip_to_instance_id_ma compliance_details_of_device_params["diff_list"] = True response = self.get_compliance_details_of_device(compliance_details_of_device_params, device_ip) - final_response[device_ip].extend(response) + if response: + final_response[device_ip].extend(response) else: # Fetch compliance details for the device without specific category compliance_details_of_device_params["device_uuid"] = device_uuid compliance_details_of_device_params["diff_list"] = True response = self.get_compliance_details_of_device(compliance_details_of_device_params, device_ip) - final_response[device_ip].extend(response) + if response: + final_response[device_ip].extend(response) # If no compliance details were found, update the result with an error message if not final_response: device_list_str = ", ".join(device_list) self.msg = "No Compliance Details found for the devices: {0}".format(device_list_str) - self.set_operation_result("failed", False, self.msg, "ERROR") - self.check_return_status() + self.fail_and_exit(self.msg) return final_response - def get_compliance_details_of_device(self, compliance_details_of_device_params, device_ip): - """ - Retrieve compliance details for a specific device. - - This function makes an API call to fetch compliance details for a given device - using the specified parameters. It handles the API response and logs the - necessary information. - - Args: - compliance_details_of_device_params (dict): Parameters required for the compliance details API call. - device_ip (str): The IP address of the device for which compliance details are being fetched. - - Returns: - dict or None: The response from the compliance details API call if successful, - None if an error occurs or no response is received. - """ - try: - # Make the API call to fetch compliance details for the device - response = self.dnac_apply["exec"]( - family="compliance", - function="compliance_details_of_device", - params=compliance_details_of_device_params, - op_modifies=True - ) - self.log("Response received post 'compliance_details_of_device' API call: {0}".format(str(response)), "DEBUG") - - if response: - response = response["response"] - else: - self.log("No response received from the 'compliance_details_of_device' API call.", "ERROR") - return response - except Exception as e: - # Handle any exceptions that occur during the API call - self.msg = ("An error occurred while retrieving Compliance Details for device:{0} using 'compliance_details_of_device' API call" - ". Error: {1}".format(device_ip, str(e))) - self.set_operation_result("failed", False, self.msg, "ERROR") - self.check_return_status() - def run_compliance(self, run_compliance_params, batch_size): """ Executes a compliance check operation in Cisco Catalyst Center. - Parameters: + Args: run_compliance_params (dict): Parameters for running the compliance check. batch_size (int): The number of devices to include in each batch. Returns: @@ -918,38 +926,18 @@ def run_compliance(self, run_compliance_params, batch_size): batch_params["deviceUuids"] = batch self.log("Batch {0} Parameters: {1}".format(idx, batch_params), "DEBUG") - try: - response = self.dnac_apply["exec"]( - family="compliance", - function="run_compliance", - params=batch_params, - op_modifies=True, - ) - self.log("Response received post 'run_compliance' API call is {0}".format(str(response)), "DEBUG") - - if response: - self.result.update(dict(response=response["response"])) - task_id = response["response"].get("taskId") - self.log("Task Id for the 'run_compliance' task is {0}".format(task_id), "DEBUG") - if task_id: - batches_dict[idx] = {"task_id": task_id, "batch_params": batch_params} - else: - self.log("No response received from the 'run_compliance' API call.", "ERROR") - - # Log and handle any exceptions that occur during the execution - except Exception as e: - msg = ( - "An error occurred while executing the 'run_compliance' operation for parameters - {0}. " - "Error: {1}".format(batch_params, str(e)) - ) - self.log(msg, "CRITICAL") + task_id = self.get_taskid_post_api_call("compliance", "run_compliance", batch_params) + if task_id: + batches_dict[idx] = {"task_id": task_id, "batch_params": batch_params} + else: + self.log("No response received from the 'run_compliance' API call for batch: {0}.".format(batch_params), "ERROR") return batches_dict def sync_device_config(self, sync_device_config_params): """ Synchronize the device configuration using the specified parameters. - Parameters: + Args: - sync_device_config_params (dict): Parameters for synchronizing the device configuration. Returns: task_id (str): The ID of the task created for the synchronization operation. @@ -959,36 +947,12 @@ def sync_device_config(self, sync_device_config_params): If an error occurs during the API call, it will be caught and logged. """ # Make an API call to synchronize device configuration - try: - response = self.dnac_apply["exec"]( - family="compliance", - function="commit_device_configuration", - params=sync_device_config_params, - op_modifies=True, - ) - self.log("Response received post 'commit_device_configuration' API call is {0}".format(str(response)), "DEBUG") - - if response: - self.result.update(dict(response=response["response"])) - self.log("Task Id for the 'commit_device_configuration' task is {0}".format(response["response"].get("taskId")), "INFO") - # Return the task ID - return response["response"].get("taskId") - else: - self.log("No response received from the 'commit_device_configuration' API call.", "ERROR") - return None - - # Log the error if an exception occurs during the API call - except Exception as e: - self.msg = ( - "Error occurred while synchronizing device configuration for parameters - {0}. " - "Error: {1}".format(sync_device_config_params, str(e))) - self.set_operation_result("failed", False, self.msg, "ERROR") - self.check_return_status() + return self.get_taskid_post_api_call("compliance", "commit_device_configuration", sync_device_config_params) def handle_error(self, task_name, mgmt_ip_to_instance_id_map, failure_reason=None): """ Handle error encountered during task execution. - Parameters: + Args: - task_name (str): Name of the task being performed. - mgmt_ip_to_instance_id_map (dict): Mapping of management IP addresses to instance IDs. - failure_reason (str, optional): Reason for the failure, if available. @@ -1013,7 +977,7 @@ def handle_error(self, task_name, mgmt_ip_to_instance_id_map, failure_reason=Non def get_batches_result(self, batches_dict): """ Retrieves the results of compliance check tasks for a list of device batches. - Parameters: + Args: batches_dict (dict): A dictionary where each key is a batch index and the value is a dictionary containing 'task_id' and 'batch_params'. Returns: @@ -1024,25 +988,40 @@ def get_batches_result(self, batches_dict): and stores the result including task ID, batch parameters, task status, and message. """ batches_result = [] - success_msg = "Task is success" + task_name = "Run Compliance" + for idx, batch_info in batches_dict.items(): task_id = batch_info["task_id"] device_ids = batch_info["batch_params"]["deviceUuids"] + success_msg = ( + "{0} Task with Task ID: '{1}' for batch number: '{2}' with {3} devices: {4} is successful." + .format(task_name, task_id, idx, len(device_ids), device_ids) + ) # Get task status for the current batch - task_status = self.get_task_status_from_tasks_by_id(task_id, device_ids, success_msg) + if self.dnac_version <= self.version_2_3_5_3: + failure_msg = ( + "{0} Task with Task ID: '{1}' Failed for batch number: '{2}' with {3} devices: {4}." + .format(task_name, task_id, idx, len(device_ids), device_ids) + ) + progress_validation = "report has been generated successfully" + self.get_task_status_from_task_by_id(task_id, task_name, failure_msg, success_msg, progress_validation=progress_validation) + else: + self.get_task_status_from_tasks_by_id(task_id, task_name, success_msg) + + 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[task_id]["msg"] - status = task_status[task_id]["status"] + # # 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, "batch_params": batch_info["batch_params"], - "task_status": status, - "msg": msg + "task_status": task_status, + "msg": success_msg } # Append the current batch result to the batches_result list @@ -1054,7 +1033,7 @@ def get_batches_result(self, batches_dict): def validate_batch_result(self, batches_result, retried_batches=None): """ Validates the results of compliance check tasks for device batches. - Parameters: + Args: batches_status (list): A list of dictionaries where each dictionary contains 'task_id', 'batch_params', 'task_status', and 'msg' for each batch. Returns: @@ -1076,7 +1055,7 @@ def validate_batch_result(self, batches_result, retried_batches=None): device_ids = tuple(batch_params.get("deviceUuids")) # Check if the task status is successful - if task_status == "Success": + if task_status == "success": successful_devices.extend(device_ids) else: # Check if the batch has already been retried with batch size of 1 @@ -1106,7 +1085,7 @@ def validate_batch_result(self, batches_result, retried_batches=None): def get_compliance_task_status(self, batches_dict, mgmt_ip_to_instance_id_map): """ Retrieves and processes compliance check task statuses for multiple batches. - Parameters: + Args: batches_dict (dict): A dictionary containing information about each batch of compliance tasks. mgmt_ip_to_instance_id_map (dict): A dictionary mapping management IP addresses to instance IDs. Returns: @@ -1134,35 +1113,46 @@ def get_compliance_task_status(self, batches_dict, mgmt_ip_to_instance_id_map): ] unsuccessful_devices = list(set(all_device_ids) - set(successful_devices)) unsuccessful_ips = [id_to_ip_map[device_id] for device_id in unsuccessful_devices if device_id in id_to_ip_map] - unsuccessful_ips_str = ", ".join(unsuccessful_ips) - self.log("{0} unsuccessful on device(s): {1}".format(task_name, unsuccessful_ips_str), "DEBUG") - if successful_devices: + final_msg = {} + if successful_devices: successful_ips = [id_to_ip_map[device_id] for device_id in successful_devices if device_id in id_to_ip_map] - successful_ips_str = ", ".join(successful_ips) - self.msg = "{0} has completed successfully on {1} device(s): {2}".format(task_name, len(successful_ips), successful_ips_str) - - if unsuccessful_ips: - msg = "{0} was unsuccessful on {1} device(s): {2}".format(task_name, len(unsuccessful_ips), unsuccessful_ips_str) - self.log(msg, "ERROR") - self.msg += "and was unsuccessful on {0} device(s): {1}".format(len(unsuccessful_ips), unsuccessful_ips_str) - + self.log("{0} Succeeded for following device(s): {1}".format(task_name, successful_ips), "INFO") + final_msg["{0} Succeeded for following device(s)".format(task_name)] = { + "success_count": len(successful_ips), + "success_devices": successful_ips + } successful_devices_params = self.want.get("run_compliance_params").copy() successful_devices_params["deviceUuids"] = successful_devices compliance_report = self.get_compliance_report(successful_devices_params, mgmt_ip_to_instance_id_map) - self.log("Compliance Report: {0}".format(compliance_report), "INFO") + self.log("Compliance Report for device on which compliance operation was successful: {0}".format(compliance_report), "INFO") + + if unsuccessful_ips: + self.log("{0} Failed for following device(s): {1}".format(task_name, unsuccessful_ips), "ERROR") + final_msg["{0} Failed for following device(s)".format(task_name)] = { + "failed_count": len(unsuccessful_ips), + "failed_devices": unsuccessful_ips + } + + self.msg = final_msg + + # Determine the final operation result + if successful_devices and unsuccessful_devices: + self.set_operation_result("failed", True, self.msg, "ERROR", compliance_report) + elif successful_devices: self.set_operation_result("success", True, self.msg, "INFO", compliance_report) + elif unsuccessful_devices: + self.set_operation_result("failed", True, self.msg, "ERROR") else: - self.msg = "Failed to {0} on the following device(s): {1}".format(task_name, unsuccessful_ips_str) - self.set_operation_result("failed", False, self.msg, "CRITICAL") + self.set_operation_result("ok", False, self.msg, "INFO") return self def get_sync_config_task_status(self, task_id, mgmt_ip_to_instance_id_map): """ This function manages the status of device configuration synchronization tasks in Cisco Catalyst Center. - Parameters: + Args: - task_id: ID of the synchronization task - mgmt_ip_to_instance_id_map: Mapping of management IP addresses to instance IDs Returns: @@ -1173,71 +1163,207 @@ 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" - start_time = time.time() - while True: - success_devices = [] - failed_devices = [] + msg = {} - response = self.check_task_tree_response(task_id, task_name) + # Retrieve the parameters for sync device config + sync_device_config_params = self.want.get("sync_device_config_params") - # Check if response returned - if not response: - self.msg = "Error retrieving Task Tree for the task_name {0} task_id {1}".format(task_name, task_id) - self.set_operation_result("failed", False, self.msg, "ERROR") - break - - # Check if the elapsed time exceeds the timeout - if self.check_timeout_and_exit(start_time, task_id, task_name, response): - break - - # Handle error if task execution encounters an error - if response[0].get("isError"): - failure_reason = response[0].get("failureReason") - self.handle_error(task_name, mgmt_ip_to_instance_id_map, failure_reason) - break - - sync_device_config_params = self.want.get("sync_device_config_params") - - for item in response[1:]: - progress = item["progress"] - for device_id in sync_device_config_params["deviceId"]: - # Get IP from mgmt_ip_to_instance_id_map - ip = next((k for k, v in mgmt_ip_to_instance_id_map.items() if v == device_id), None) - if device_id in progress and "copy_Running_To_Startup=Success" in progress: - success_devices.append(ip) - self.log("{0} operation completed successfully on device: {1} with device UUID: {2}".format(task_name, ip, device_id), "DEBUG") - elif device_id in progress and "copy_Running_To_Startup=Failed" in progress: - self.log("{0} operation FAILED on device: {1} with device UUID: {2}".format(task_name, ip, device_id), "DEBUG") - failed_devices.append(ip) - - success_devices = set(success_devices) - success_devices_str = ", ".join(success_devices) - failed_devices = set(failed_devices) - failed_devices_str = ", ".join(failed_devices) - - # Check conditions and print messages accordingly - total_devices = len(sync_device_config_params["deviceId"]) - if len(success_devices) == total_devices: - self.msg = "{0} has completed successfully on {1} device(s): {2}".format(task_name, len(success_devices), success_devices_str) - self.set_operation_result("success", True, self.msg, "INFO") - break - elif failed_devices and len(failed_devices) + len(success_devices) == total_devices: - self.msg = "{0} task has failed on {1} device(s): {2} and succeeded on {3} device(s): {4}".format( - task_name, len(failed_devices), failed_devices_str, len(success_devices), success_devices_str) - self.set_operation_result("failed", True, self.msg, "CRITICAL") - break - elif len(failed_devices) == total_devices: - self.msg = "{0} task has failed on {1} device(s): {2}".format(task_name, len(failed_devices), failed_devices_str) - self.set_operation_result("failed", False, self.msg, "CRITICAL") - break + # Extract the list of device IDs from sync_device_config_params + device_ids = sync_device_config_params.get("deviceId") + + # 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] + + msg["{0} Succeeded for following device(s)".format(task_name)] = {"success_count": len(device_ip_list), "success_devices": device_ip_list} + + # Retrieve and return the task status using the provided task ID + return self.get_task_status_from_tasks_by_id(task_id, task_name, msg) + + def process_final_result(self, final_status_list): + """ + Processes a list of final statuses and returns a tuple indicating the result and a boolean flag. + Args: + final_status_list (list): List of status strings to process. + Returns: + tuple: A tuple containing a status string ("ok" or "success") and a boolean flag (False if all statuses are "ok", True otherwise). + """ + status_set = set(final_status_list) + + if status_set == {"ok"}: + return "ok", False + else: + return "success", True + + def verify_sync_device_config(self): + """ + Verify the success of the "Sync Device Configuration" operation. + Args: + config (dict): A dictionary containing the configuration details. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This method verifies the success of the "Sync Device Configuration" operation in the context of network compliance management. + It checks if the configuration includes the option to synchronize device configurations (`sync_device_config`). + If this option is present, the function proceeds to compare compliance details before and after executing the synchronization operation. + It logs relevant information at each step and concludes by determining whether the synchronization was successful. + """ + # Get compliance details before running sync_device_config + compliance_details_before = self.want.get("compliance_details") + self.log("Compliance details before running sync_device_config: {0}".format(compliance_details_before), "INFO") + + # Get compliance details after running sync_device_config + compliance_details_after = self.get_compliance_report( + self.want.get("compliance_detail_params_sync"), + self.want.get("mgmt_ip_to_instance_id_map") + ) + if not compliance_details_after: + self.msg = "Error occurred when Retrieving Compliance Details after for verifying configuration." + self.fail_and_exit(self.msg) + + self.log("Compliance details after running sync_device_config: {0}.".format(compliance_details_after), "INFO") + + # Get the device IDs to check + sync_device_ids = self.want.get("sync_device_config_params").get("deviceId", []) + if not sync_device_ids: + self.log( + "No device IDs found in sync_device_config_params, Sync Device Configuration " + "operation may not have been performed.", + "WARNING" + ) + return self + + # Initialize the status lists + all_statuses_before = [] + all_statuses_after = [] + + # Iterate over the device IDs and check their compliance status + 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) + if ip_address: + # Get the status before + compliance_before = compliance_details_before.get(ip_address, []) + if compliance_before: + all_statuses_before.append(compliance_before[0]["status"]) + # Get the status after + compliance_after = compliance_details_after.get(ip_address, []) + if compliance_after: + all_statuses_after.append(compliance_after[0]["status"]) + + # Check if all statuses changed from "NON_COMPLIANT" to "COMPLIANT" + if ( + all(all_status == "NON_COMPLIANT" for all_status in all_statuses_before) and + all(all_status == "COMPLIANT" for all_status in all_statuses_after) + ): + self.log("Verified the success of the Sync Device Configuration operation.") + else: + self.log( + "Sync Device Configuration operation may have been unsuccessful " + "since not all devices have 'COMPLIANT' status after the operation.", + "WARNING" + ) + + def get_want(self, config): + """ + Determines the desired state based on the provided configuration. + Args: + config (dict): The configuration specifying the desired state. + Returns: + dict: A dictionary containing the desired state parameters. + Description: + This method processes the provided configuration to determine the desired state. It validates the presence of + either "ip_address_list" or "site_name" and constructs parameters for running compliance checks and syncing + device configurations based on the provided configuration. It also logs the desired state for reference. + """ + # Initialize parameters + run_compliance_params = {} + sync_device_config_params = {} + compliance_detail_params_sync = {} + compliance_details = {} + + # Store input parameters + ip_address_list = config.get("ip_address_list") + # Remove Duplicates from list + if ip_address_list: + ip_address_list = list(set(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") + + # Validate the provided configuration parameters + self.validate_params(config) + + # Retrieve device ID list + mgmt_ip_to_instance_id_map = self.get_device_id_list(ip_address_list, site_name) + + # 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: + 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"] + } + + response = self.get_compliance_report(compliance_detail_params_sync, mgmt_ip_to_instance_id_map) + 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 " + msg += "is required on device(s): {0}".format(ip_address_list_str) + self.fail_and_exit(self.msg) + + compliance_details = response + sync_required, self.msg, categorized_devices = self.is_sync_required(response, mgmt_ip_to_instance_id_map) + self.log("Is Sync Requied: {0} -- Message: {1}".format(sync_required, self.msg), "DEBUG") + if sync_required: + sync_device_config_params = self.get_sync_device_config_params(mgmt_ip_to_instance_id_map, categorized_devices) + self.log( + "Sync Device Configuration operation is required for provided parameters in the Cisco Catalyst Center." + "therefore setting 'sync_device_config_params' - {0}.".format(sync_device_config_params), + "DEBUG" + ) + else: + self.log( + "Sync Device Configuration operation is not required for provided parameters in the Cisco Catalyst Center." + "therefore not setting the 'sync_device_config_params'", + "INFO" + ) + self.skipped_sync_device_configs_list.extend(list(mgmt_ip_to_instance_id_map.keys())) + else: + self.msg = ( + "The specified Cisco Catalyst Center version: '{0}' does not support the Sync Device Config operation. " + "Supported version start '2.3.7.6' onwards. Version '2.3.5.3' introduces APIs for " + "performing Compliance Checks. Version '2.3.7.6' expands support to include APIs " + "for Compliance Checks and Sync Device Config operations.".format(self.get_ccc_version()) + ) + self.fail_and_exit(self.msg) + + # Construct the "want" dictionary containing the desired state parameters + want = {} + want = dict( + ip_address_list=ip_address_list, + site_name=site_name, + mgmt_ip_to_instance_id_map=mgmt_ip_to_instance_id_map, + run_compliance_params=run_compliance_params, + run_compliance_batch_size=config.get("run_compliance_batch_size"), + sync_device_config_params=sync_device_config_params, + compliance_detail_params_sync=compliance_detail_params_sync, + compliance_details=compliance_details + ) + self.want = want + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") return self - def get_diff_merged(self): + def get_diff_merged(self, config): """ This method is designed to Perform Network Compliance Actions in Cisco Catalyst Center. - Parameters: None + Args: None Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: @@ -1245,36 +1371,68 @@ def get_diff_merged(self): It ensures all required tasks are present, executes them, and checks their status, facilitating smooth playbook execution. """ # Action map for different network compliance operations + self.log("Starting 'get_diff_merged' operation.", "INFO") + action_map = { "run_compliance_params": (self.run_compliance, self.get_compliance_task_status), "sync_device_config_params": (self.sync_device_config, self.get_sync_config_task_status) } + # Check if all action_map keys are missing in self.want + if not any(action_param in self.want for action_param in action_map.keys()): + self.msg = "Network Compliance operation(s) are not required for the provided input parameters in the Cisco Catalyst Center." + self.set_operation_result("ok", False, self.msg, "INFO") + return self + + final_status_list = [] + result_details = {} + # Iterate through the action map and execute specified actions for action_param, (action_func, status_func) in action_map.items(): - + req_action_param = self.want.get(action_param) # Execute the action and check its status - if self.want.get(action_param): + if req_action_param: + self.log("Executing action function: {0} with params: {1}".format(action_func.__name__, req_action_param), "INFO") if action_param == "run_compliance_params": - self.log("Performing '{0}' operation...".format(action_func.__name__), "DEBUG") batch_size = self.want.get("run_compliance_batch_size") result_task_id = action_func(self.want.get(action_param), batch_size=batch_size) else: - self.log("Performing '{0}' operation...".format(action_func.__name__), "DEBUG") result_task_id = action_func(self.want.get(action_param)) if not result_task_id: self.msg = "An error occurred while retrieving the task_id of the {0} operation.".format(action_func.__name__) self.set_operation_result("failed", False, self.msg, "CRITICAL") else: + self.log("Task Id: {0} returned from the action function: {1}".format(result_task_id, action_func.__name__), "DEBUG") status_func(result_task_id, self.want.get("mgmt_ip_to_instance_id_map")).check_return_status() + result = self.msg + result_details.update(result) + if config.get("sync_device_config"): + skipped_sync_device_configs_list = set(self.skipped_sync_device_configs_list) + if skipped_sync_device_configs_list: + 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 + } + + skipped_run_compliance_devices_list = set(self.skipped_run_compliance_devices_list) + if skipped_run_compliance_devices_list: + 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 + } + + final_status, is_changed = self.process_final_result(final_status_list) + self.msg = result_details + self.log("Completed 'get_diff_merged' operation with final status: {0}, is_changed: {1}".format(final_status, is_changed), "INFO") + self.set_operation_result(final_status, is_changed, self.msg, "INFO", self.result.get("data")) return self def verify_diff_merged(self, config): """ Verify the success of the "Sync Device Configuration" operation. - Parameters: + Args: config (dict): A dictionary containing the configuration details. Returns: self (object): An instance of a class used for interacting with Cisco Catalyst Center. @@ -1284,65 +1442,20 @@ def verify_diff_merged(self, config): If this option is present, the function proceeds to compare compliance details before and after executing the synchronization operation. It logs relevant information at each step and concludes by determining whether the synchronization was successful. """ - if config.get("sync_device_config"): - # Get compliance details before running sync_device_config - compliance_details_before = self.want.get("compliance_details") - self.log("Compliance details before running sync_device_config: {0}".format(compliance_details_before), "INFO") - - # Get compliance details after running sync_device_config - compliance_details_after = self.get_compliance_report( - self.want.get("compliance_detail_params_sync"), - self.want.get("mgmt_ip_to_instance_id_map") - ) - if not compliance_details_after: - self.msg = "Error occured when Retrieving Compliance Details after for verifying configuration." - self.update("failed", False, self.msg, "ERROR") - self.check_return_status() - - self.log("Compliance details after running sync_device_config: {0}.".format(compliance_details_after), "INFO") - - # Get the device IDs to check - sync_device_ids = self.want.get("sync_device_config_params").get("deviceId", []) - if not sync_device_ids: - self.log( - "No device IDs found in sync_device_config_params, Sync Device Configuration " - "operation may not have been performed.", - "WARNING" - ) - return self - - # Initialize the status lists - all_statuses_before = [] - all_statuses_after = [] - - # Iterate over the device IDs and check their compliance status - 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) - if ip_address: - # Get the status before - compliance_before = compliance_details_before.get(ip_address, []) - if compliance_before: - all_statuses_before.append(compliance_before[0]["status"]) - # Get the status after - compliance_after = compliance_details_after.get(ip_address, []) - if compliance_after: - all_statuses_after.append(compliance_after[0]["status"]) - - # Check if all statuses changed from "NON_COMPLIANT" to "COMPLIANT" - if ( - all(all_status == "NON_COMPLIANT" for all_status in all_statuses_before) and - all(all_status == "COMPLIANT" for all_status in all_statuses_after) - ): - self.log("Verified the success of the Sync Device Configuration operation.") - else: - self.log( - "Sync Device Configuration operation may have been unsuccessful " - "since not all devices have 'COMPLIANT' status after the operation.", - "WARNING" - ) - else: - self.log("Verification of configuration is not required for run compliance operation!", "INFO") + self.log("Starting 'verify_diff_merged' operation.", "INFO") + + sync_device_config_params = self.want.get("sync_device_config_params") + run_compliance_params = self.want.get("run_compliance_params") + + if sync_device_config_params: + self.log("Starting verification of Sync Device Configuration operation.", "INFO") + self.verify_sync_device_config() + self.log("Completed verification of Sync Device Configuration operation.", "INFO") + + if run_compliance_params: + self.log("Verification of configuration is not required for Run Compliance operation!", "INFO") + + self.log("Completed 'verify_diff_merged' operation.", "INFO") return self @@ -1396,7 +1509,7 @@ def main(): # Iterate over the validated configuration parameters for config in ccc_network_compliance.validated_config: ccc_network_compliance.get_want(config).check_return_status() - ccc_network_compliance.get_diff_state_apply[state]().check_return_status() + ccc_network_compliance.get_diff_state_apply[state](config).check_return_status() if config_verify: ccc_network_compliance.verify_diff_state_apply[state](config).check_return_status() diff --git a/tests/integration/ccc_device_configs_backup_management/tests/test_device_configs_backup_management.yml b/tests/integration/ccc_device_configs_backup_management/tests/test_device_configs_backup_management.yml index fe94ba2130..ae48e23d24 100644 --- a/tests/integration/ccc_device_configs_backup_management/tests/test_device_configs_backup_management.yml +++ b/tests/integration/ccc_device_configs_backup_management/tests/test_device_configs_backup_management.yml @@ -34,10 +34,10 @@ loop: "{{ vars_map.device_backup }}" register: result_device_backup - # - name: Debug item - # debug: - # var: item - # loop: "{{ result_device_backup.results }}" + - name: Debug item + debug: + var: item + loop: "{{ result_device_backup.results }}" - name: Assert device configuration backup assert: @@ -56,6 +56,15 @@ register: cleanup_result delegate_to: localhost + - name: Delete the generated file + file: + path: "{{ item.ansible_facts.file_path | default('tmp') }}" + state: absent + when: item.changed + loop: "{{ result_device_backup.results }}" + register: cleanup_result + delegate_to: localhost + - name: Print cleanup result debug: var: cleanup_result From 704bcbf2d6fdf67ce7c24c44829c66d5e7cd3753 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sat, 19 Oct 2024 18:01:34 -0700 Subject: [PATCH 2/7] network compliance, device config backups bug fixes and dnac.py update --- plugins/module_utils/dnac.py | 16 ++++++---------- .../device_configs_backup_workflow_manager.py | 4 ++-- .../network_compliance_workflow_manager.py | 16 +++++++++++----- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index a3f312a4b2..4f40717356 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -749,19 +749,15 @@ def get_reachable_devices_from_site(self, site_id): mgmt_ip_to_instance_id_map[management_ip] = instance_uuid else: skipped_devices_list.append(management_ip) - msg = ( - "Skipping device {0} as its family is: {1}.".format( - management_ip, device_family - ) + msg = "Skipping device {0} as its family is: {1}.".format( + management_ip, device_family ) self.log(msg, "WARNING") else: skipped_devices_list.append(management_ip) - msg = ( - "Skipping device {0} as its status is {1} or its collectionStatus is {2}.".format( - management_ip, reachability_status, collection_status - ) - ) + msg = "Skipping device {0} as its status is {1} or its collectionStatus is {2}.".format( + management_ip, reachability_status, collection_status + ) self.log(msg, "WARNING") if not mgmt_ip_to_instance_id_map: @@ -1641,7 +1637,7 @@ 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 - # Check if there is an error in the task response + # Check if there is an error in the task response if response.get("isError"): failure_reason = response.get("failureReason") self.msg = failure_reason diff --git a/plugins/modules/device_configs_backup_workflow_manager.py b/plugins/modules/device_configs_backup_workflow_manager.py index 9747ec4c11..d85f79d857 100644 --- a/plugins/modules/device_configs_backup_workflow_manager.py +++ b/plugins/modules/device_configs_backup_workflow_manager.py @@ -894,7 +894,7 @@ def get_export_device_config_task_status(self, task_id): failure_msg = ( "An error occurred while performing {0} task with task ID {1} for export_device_configurations_params: {2}" .format(task_name, task_id, self.want.get("export_device_configurations_params")) - ) + ) self.get_task_status_from_task_by_id(task_id, task_name, failure_msg, success_msg, progress_validation=progress_validation) else: self.get_task_status_from_tasks_by_id(task_id, task_name, success_msg) @@ -909,7 +909,7 @@ 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) - fail_and_exit(self.msg) + self.fail_and_exit(self.msg) # Perform additional tasks after breaking the loop mgmt_ip_to_instance_id_map = self.want.get("mgmt_ip_to_instance_id_map") diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 540e9a4f2a..19d3192f9a 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -748,8 +748,10 @@ def get_device_id_list(self, ip_address_list, site_name): if not mgmt_ip_to_instance_id_map: # Log an error message if mgmt_ip_to_instance_id_map is empty ip_address_list_str = ", ".join(ip_address_list) - self.msg = ("No device UUIDs were fetched for network compliance operations with the provided IP address(es): {0} " - "or site name: {1}. This could be due to Unreachable devices or access points (APs).").format(ip_address_list_str, site_name) + self.msg = ( + "No device UUIDs were fetched for network compliance operations with the provided IP address(es): {0} " + "or site name: {1}. This could be due to Unreachable devices or access points (APs)." + ).format(ip_address_list_str, site_name) self.fail_and_exit(self.msg) return mgmt_ip_to_instance_id_map @@ -822,9 +824,13 @@ def get_compliance_details_of_device(self, compliance_details_of_device_params, self.log("Sucessfully retrieved Compliance details for device: '{0}'".format(device_ip), "INFO") return response.get("response") else: + self.log( + "No Compliance details retrieved for device: '{0}' with parameters: {1}".format( + device_ip, compliance_details_of_device_params + ), + "WARNING" + ) return None - # self.msg = "An error occurred while retrieving Compliance Details for device: '{0}'".format(device_ip) - # self.fail_and_exit(self.msg) def get_compliance_report(self, run_compliance_params, mgmt_ip_to_instance_id_map): """ @@ -1314,7 +1320,7 @@ def get_want(self, config): 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 " - msg += "is required on device(s): {0}".format(ip_address_list_str) + self.msg += "is required on device(s): {0}".format(ip_address_list_str) self.fail_and_exit(self.msg) compliance_details = response From 92e0eabb841d0c8b2e31f41781e9e935a0c6bb00 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Mon, 21 Oct 2024 14:13:11 -0700 Subject: [PATCH 3/7] 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 From 1466054d58a958bb7283210c537e9e8841311867 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Mon, 21 Oct 2024 14:56:42 -0700 Subject: [PATCH 4/7] addressed PR review comments --- plugins/module_utils/dnac.py | 21 +++++++++++++++---- .../network_compliance_workflow_manager.py | 10 ++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index d670cc083f..d3d6b4aebc 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -848,7 +848,10 @@ def get_site_id(self, site_name): site_exists = True except Exception as 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.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) @@ -1619,7 +1622,12 @@ def get_task_status_from_tasks_by_id(self, task_id, task_name, success_msg): 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") + 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) @@ -1692,7 +1700,12 @@ def get_task_status_from_task_by_id(self, task_id, task_name, failure_msg, succe # 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") + 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 @@ -1701,7 +1714,7 @@ def get_task_status_from_task_by_id(self, task_id, task_name, failure_msg, succe end_time = response.get("endTime") self.log("Current task progress for '{0}': {1}, Data: {2}".format(task_name, progress, data), "INFO") - # Validate task data or progress if validation keys are provided + # 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 diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 74195970e5..dc1851a65d 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -481,9 +481,13 @@ def validate_compliance_operation(self, run_compliance, run_compliance_categorie 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") + 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 = ( From e7e205038dd736bc9713fd81b86fe1643d20b155 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Mon, 21 Oct 2024 15:48:26 -0700 Subject: [PATCH 5/7] addressed PR review comments --- plugins/module_utils/dnac.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index d3d6b4aebc..66295773e1 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -1625,7 +1625,7 @@ def get_task_status_from_tasks_by_id(self, task_id, task_name, success_msg): 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 @@ -1703,7 +1703,7 @@ def get_task_status_from_task_by_id(self, task_id, task_name, failure_msg, succe 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 From 334df52bc1a91c6046f9e449de40d74df8a43f75 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Mon, 21 Oct 2024 15:54:47 -0700 Subject: [PATCH 6/7] addressed PR review comments --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 66295773e1..28c79fbfd9 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -708,7 +708,7 @@ def get_device_details_from_site(self, site_name, site_id=None): 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 + return device_details_list self.log("Retrieved site ID '{0}' for site name '{1}'.".format(site_id, site_name), "DEBUG") From 4060b9c3ea8d66b270e340ebd1b63a4366da1528 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Tue, 22 Oct 2024 00:04:28 -0700 Subject: [PATCH 7/7] spelling mistake fix for sda_host_port_onboarding module --- .../modules/sda_host_port_onboarding_workflow_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/sda_host_port_onboarding_workflow_manager.py b/plugins/modules/sda_host_port_onboarding_workflow_manager.py index 4a9d4ecbad..27a4ce8cf8 100644 --- a/plugins/modules/sda_host_port_onboarding_workflow_manager.py +++ b/plugins/modules/sda_host_port_onboarding_workflow_manager.py @@ -314,7 +314,7 @@ - interface_names: ["TenGigabitEthernet1/1/2", "TenGigabitEthernet1/1/3", "TenGigabitEthernet1/1/4"] connected_device_type: "TRUNK" protocol: "PAGP" - port_channel_descrption: "Trunk port channel" + port_channel_description: "Trunk port channel" - interface_names: ["TenGigabitEthernet1/1/5", "TenGigabitEthernet1/1/6"] connected_device_type: "EXTENDED_NODE" @@ -322,7 +322,7 @@ - interface_names: ["TenGigabitEthernet1/1/7", "TenGigabitEthernet1/1/8"] connected_device_type: "EXTENDED_NODE" protocol: "PAGP" - port_channel_descrption: "extended node port channel" + port_channel_description: "extended node port channel" - name: Update port interfaces and port channels for a specific fabric device cisco.dnac.sda_host_port_onboarding_workflow_manager: @@ -360,7 +360,7 @@ - interface_names: ["TenGigabitEthernet1/1/2", "TenGigabitEthernet1/1/3", "TenGigabitEthernet1/1/4"] connected_device_type: "EXTENDED_NODE" protocol: 'PAGP' - port_channel_descrption: "Trunk port channel" + port_channel_description: "Trunk port channel" - name: Delete ALL port assignments and port channels for the fabric device using hostname cisco.dnac.sda_host_port_onboarding_workflow_manager: