diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index daf83e452a..b6fff973e6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,7 +18,7 @@ jobs: git config user.email "${GITHUB_ACTOR}@bots.github.com" git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: "3.7" - name: Install dependencies diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index d0c655d8e3..8d3f5fb602 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -1038,3 +1038,29 @@ releases: - device_credential_workflow_manager.py - added attribute 'apply_credentials_to_site'. - accesspoint_workflow_manager.py - added attribute 'factory_reset_aps'. - Ansible utils requirement updated. + 6.22.0: + release_date: "2024-10-25" + changes: + release_summary: Code changes in workflow manager modules. + minor_changes: + - Added 'lan_automation_workflow_manager' to automate network discovery, deployment, and device configuration with LAN Automation. + - Added 'sda_fabric_devices_workflow_manager' to manage SDA fabric devices. + - Bug fixes in accesspoint_workflow_manager module + - Changes in device_configs_backup_workflow_manager module + - Code enhancements in device_credential_workflow_manager module + - Enhancements in ise_radius_integration_workflow_manager module + - Code changes in network_compliance_workflow_manager module + - Bug fixes in network_settings_workflow_manager module + - Bug fixes in pnp_workflow_manager module + - Code changes in rma_workflow_manager module + - Code changes in sda_fabric_devices_workflow_manager module + - Code changes in sda_fabric_sites_zones_workflow_manager module + - Code changes in sda_fabric_virtual_networks_workflow_manager module + - Code changes in sda_host_port_onboarding_workflow_manager module + - Code changes in site_workflow_manager module + - Code changes in swim_workflow_manager module + - Code change in template_workflow_manager module + - Code change in user_role_manager module + - Changes in dnac.py + - inventory_workflow_manager.py - added attribute hostnames, serial_numbers and mac_addresses + - inventory_workflow_manager.py - Removed attribute hostname_list, serial_number_list and mac_address_list diff --git a/galaxy.yml b/galaxy.yml index 7f8340802c..eee72fc976 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.21.0 +version: 6.22.0 readme: README.md authors: - Rafael Campos diff --git a/playbooks/template_workflow_manager.yml b/playbooks/template_workflow_manager.yml index be296ff2e9..17d3127d4a 100644 --- a/playbooks/template_workflow_manager.yml +++ b/playbooks/template_workflow_manager.yml @@ -12,12 +12,13 @@ dnac_username: "{{ dnac_username }}" dnac_password: "{{ dnac_password }}" dnac_verify: "{{ dnac_verify }}" + dnac_version: "{{dnac_version}}" dnac_debug: "{{ dnac_debug }}" - dnac_log: True + dnac_log: true dnac_log_level: DEBUG - dnac_log_append: True - dnac_log_file_path: "{{ dnac_log_file_path }}" - validate_response_schema: False + dnac_log_append: true + # dnac_log_file_path: "{{ dnac_log_file_path }}" + validate_response_schema: false state: "merged" config_verify: true #ignore_errors: true #Enable this to continue execution even the task fails @@ -41,6 +42,30 @@ import: project: "{{ item.import_project }}" template: "{{ item.import_template }}" + + deploy_template: + project_name: "{{ item.proj_name }}" + template_name: "{{ item.temp_name }}" + force_push: "{{ item.force_push }}" + template_parameters: + - param_name: "{{ item.template_parameters.param_name }}" + param_value: "{{ item.template_parameters.param_value }}" + - param_name: "{{ item.template_parameters.param_name }}" + param_value: "{{ item.template_parameters.param_value }}" + device_details: + # Provide any of the one device_specific details either device_ips, device_hostnames + # serial_numbers, mac_addresses to deploy template to the devices + # device_ips: "{{ item.device_details.device_ips }}" + device_hostnames: "{{ item.device_details.device_hostnames }}" + # serial_numbers: "{{ item.device_details.serial_numbers }}" + # mac_addresses: "{{ item.device_details.mac_addresses }}" + site_provisioning_details: + # Provide the site name and other parameters are optional to narrow down the results + - site_name: "{{ item.site_provisioning_details.site_name }}" + device_family: "{{ item.site_provisioning_details.device_family }}" + device_role: "{{ item.site_provisioning_details.device_role }}" + device_tag: "{{ item.site_provisioning_details.device_tag }}" + register: template_result with_items: '{{ template_details }}' tags: diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index e17c3df305..90e2e438ba 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -384,26 +384,38 @@ 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: {0}, Response: {1}" + .format(task_id, 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("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() + 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 +430,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 """ @@ -496,38 +506,42 @@ def reset_values(self): self.have.clear() self.want.clear() - def get_execution_details(self, execid): + def get_execution_details(self, exec_id): """ Get the execution details of an API - - Parameters: - execid (str) - Id for API execution - + Args: + exec_id (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": exec_id} + ) + self.log("Successfully retrieved execution details by the API 'get_business_api_execution_details' for execution ID: {0}, Response: {1}" + .format(exec_id, 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 +587,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 +601,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 +614,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 +629,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): + 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: 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. @@ -684,83 +647,151 @@ def get_device_ids_from_site(self, site_id): device_ids = [] api_response = None - 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 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 - 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 site ID '{0}' for site name '{1}'.".format(site_id, site_name), "DEBUG") - self.log("Retrieved device IDs from membership for site '{0}': {1}".format(site_id, device_ids), "DEBUG") + self.log("Initiating retrieval of device IDs for site ID: '{0}'.".format(site_id), "DEBUG") - except Exception as e: - self.log("Error retrieving device IDs from membership for site '{0}': {1}".format(site_id, str(e)), "ERROR") + # 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) - else: - try: - api_response = self.dnac._exec( - family="site_design", - function="get_site_assigned_network_devices", - op_modifies=True, - params={"site_id": site_id}, - ) + 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", []): + device_ids.append(item.get("instanceUuid")) - 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 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("Retrieved device IDs from assigned devices for site '{0}': {1}".format(site_id, device_ids), "DEBUG") + 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")) - 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") + 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_site_id(self, site_name): + def get_device_details_from_site(self, site_name, site_id=None): """ - 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. + 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") + + # 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 device_details_list + + 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_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) - 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'. + self.log("Device IDs retrieved from site '{0}': {1}".format(site_id, str(device_ids)), "DEBUG") + + # 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_name): + """ + 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 = [] - 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 + (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) - 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("Initiating retrieval of device details for site ID: '{0}'.".format(site_id), "INFO") - return (site_exists, site_id) + # Retrieve the list of device details from the specified site + 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 + 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 +806,66 @@ 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 + 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) - 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 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) - 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 +989,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 +1012,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. """ @@ -1001,13 +1034,352 @@ def update_site_type_key(self, config): return new_config - def is_valid_ipv4(self, ip_address): + def get_device_ips_from_hostnames(self, hostnames): + """ + Get the list of unique device IPs for list of specified hostnames of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + hostnames (list): The hostnames of devices for which you want to retrieve the device IPs. + Returns: + device_ip_mapping (dict): Provide the dictionary with the mapping of hostname of device to its ip address. + Description: + Queries Cisco Catalyst Center to retrieve the unique device IP's associated with a device having the specified + list of hostnames. If a device is not found in Cisco Catalyst Center, an error log message is printed. """ - Validates an IPv4 address. + self.log("Entering 'get_device_ips_from_hostnames' with hostname_list: {0}".format(str(hostnames)), "INFO") + device_ip_mapping = {} + + for hostname in hostnames: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=True, + params={"hostname": hostname} + ) + if response: + self.log("Received API response for hostname '{0}': {1}".format(hostname, str(response)), "DEBUG") + response = response.get("response") + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ip_mapping[hostname] = device_ip + self.log("Added device IP '{0}' for hostname '{1}'.".format(device_ip, hostname), "INFO") + else: + device_ip_mapping[hostname] = None + self.log("No management IP found for hostname '{0}'.".format(hostname), "WARNING") + else: + device_ip_mapping[hostname] = None + self.log("No response received for hostname '{0}'.".format(hostname), "WARNING") + else: + device_ip_mapping[hostname] = None + self.log("No response received from 'get_device_list' for hostname '{0}'.".format(hostname), "ERROR") + + except Exception as e: + error_message = "Exception occurred while fetching device IP for hostname '{0}': {1}".format(hostname, str(e)) + self.log(error_message, "ERROR") + device_ip_mapping[hostname] = None + + self.log("Exiting 'get_device_ips_from_hostnames' with device IP mapping: {0}".format(device_ip_mapping), "INFO") + return device_ip_mapping + + def get_device_ips_from_serial_numbers(self, serial_numbers): + """ + Get the list of unique device IPs for a specified list of serial numbers in Cisco Catalyst Center. Parameters: - ip_address - String denoting the IPv4 address passed. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + serial_numbers (list): The list of serial number of devices for which you want to retrieve the device IPs. + Returns: + device_ip_mapping (dict): Provide the dictionary with the mapping of serial number of device to its ip address. + Description: + Queries Cisco Catalyst Center to retrieve the unique device IPs associated with a device having the specified + serial numbers.If a device is not found in Cisco Catalyst Center, an error log message is printed. + """ + + self.log("Entering 'get_device_ips_from_serial_numbers' with serial_numbers: {0}".format(str(serial_numbers)), "INFO") + device_ip_mapping = {} + + for serial_number in serial_numbers: + try: + self.log("Fetching device info for serial number: {0}".format(serial_number), "INFO") + response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=True, + params={"serialNumber": serial_number} + ) + if response: + self.log("Received API response for serial number '{0}': {1}".format(serial_number, str(response)), "DEBUG") + response = response.get("response") + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ip_mapping[serial_number] = device_ip + self.log("Added device IP '{0}' for serial number '{1}'.".format(device_ip, serial_number), "INFO") + else: + device_ip_mapping[serial_number] = None + self.log("No management IP found for serial number '{0}'.".format(serial_number), "WARNING") + else: + device_ip_mapping[serial_number] = None + self.log("No response received for serial number '{0}'.".format(serial_number), "WARNING") + else: + device_ip_mapping[serial_number] = None + self.log("No response received from 'get_device_list' for serial number '{0}'.".format(serial_number), "ERROR") + + except Exception as e: + error_message = "Exception occurred while fetching device IP for serial number '{0}': {1}".format(serial_number, str(e)) + self.log(error_message, "ERROR") + device_ip_mapping[serial_number] = None + + self.log("Exiting 'get_device_ips_from_serial_numbers' with device IP mapping: {0}".format(device_ip_mapping), "INFO") + return device_ip_mapping + + def get_device_ips_from_mac_addresses(self, mac_addresses): + """ + Get the list of unique device IPs for list of specified mac address of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + mac_addresses (list): The list of mac address of devices for which you want to retrieve the device IPs. + Returns: + device_ip_mapping (dict): Provide the dictionary with the mapping of mac address of device to its ip address. + Description: + Queries Cisco Catalyst Center to retrieve the unique device IPs associated with a device having the specified + mac addresses. If a device is not found in Cisco Catalyst Center, an error log message is printed. + """ + + self.log("Entering 'get_device_ips_from_mac_addresses' with mac_addresses: {0}".format(str(mac_addresses)), "INFO") + device_ip_mapping = {} + + for mac_address in mac_addresses: + try: + self.log("Fetching device info for mac_address: {0}".format(mac_address), "INFO") + response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=True, + params={"macAddress": mac_address} + ) + if response: + self.log("Received API response for mac address '{0}': {1}".format(mac_address, str(response)), "DEBUG") + response = response.get("response") + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ip_mapping[mac_address] = device_ip + self.log("Added device IP '{0}' for mac address '{1}'.".format(device_ip, mac_address), "INFO") + else: + device_ip_mapping[mac_address] = None + self.log("No management IP found for mac address '{0}'.".format(mac_address), "WARNING") + else: + device_ip_mapping[mac_address] = None + self.log("No response received for mac address '{0}'.".format(mac_address), "WARNING") + else: + device_ip_mapping[mac_address] = None + self.log("No response received from 'get_device_list' for mac address '{0}'.".format(mac_address), "ERROR") + + except Exception as e: + error_message = "Exception occurred while fetching device IP for mac address '{0}': {1}".format(mac_address, str(e)) + self.log(error_message, "ERROR") + device_ip_mapping[mac_address] = None + + self.log("Exiting 'get_device_ips_from_mac_addresses' with device IP mapping: {0}".format(device_ip_mapping), "INFO") + return device_ip_mapping + + def get_device_ids_from_device_ips(self, device_ips): + """ + Get the list of unique device IPs for list of specified hostnames of devices in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + hostname_list (list): The hostnames of devices for which you want to retrieve the device IPs. + Returns: + device_id_mapping (dict): Provide the dictionary with the mapping of ip address of device to device id. + Description: + Queries Cisco Catalyst Center to retrieve the unique device IP's associated with a device having the specified + list of hostnames. If a device is not found in Cisco Catalyst Center, an error log message is printed. + """ + + self.log("Entering 'get_device_ids_from_device_ips' with device ips: {0}".format(str(device_ips)), "INFO") + device_id_mapping = {} + + for device_ip in device_ips: + try: + self.log("Fetching device id for device ip: {0}".format(device_ip), "INFO") + response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=False, + params={"management_ip_address": device_ip} + ) + if response: + self.log("Received API response for device ip '{0}': {1}".format(device_ip, str(response)), "DEBUG") + response = response.get("response") + if response: + device_id = response[0]["id"] + if device_id: + device_id_mapping[device_ip] = device_id + self.log("Added device ID '{0}' for device ip '{1}'.".format(device_id, device_ip), "INFO") + else: + device_id_mapping[device_ip] = None + self.log("No device ID found for device ip '{0}'.".format(device_ip), "WARNING") + else: + device_id_mapping[device_ip] = None + self.log("No response received for device ip '{0}'.".format(device_ip), "WARNING") + else: + device_id_mapping[device_ip] = None + self.log("No response received from 'get_device_list' for device ip '{0}'.".format(device_ip), "ERROR") + + except Exception as e: + error_message = "Exception occurred while fetching device ID for device ip '{0}': {1}".format(device_ip, str(e)) + self.log(error_message, "ERROR") + device_id_mapping[device_ip] = None + + self.log("Exiting 'get_device_ids_from_device_ips' with unique device ID mapping: {0}".format(device_id_mapping), "INFO") + return device_id_mapping + + def get_device_ips_from_device_ids(self, device_ids): + """ + Retrieves the management IP addresses of devices based on the provided device IDs. + + Args: + device_ids (list): A list of device IDs for which the management IP addresses need to be fetched. + Returns: + device_ip_mapping (dict): Provide the dictionary with the mapping of id of device to its ip address. + Description: + This function iterates over a list of device IDs, makes an API call to Cisco Catalyst Center to fetch + the management IP addresses of the devices, and returns a list of these IPs. If a device is not found + or an exception occurs, it logs the error or warning and continues to the next device ID. + """ + + self.log("Entering 'get_device_ips_from_device_ids' with device ips: {0}".format(str(device_ids)), "INFO") + device_ip_mapping = {} + + for device_id in device_ids: + try: + self.log("Fetching device ip for device id: {0}".format(device_id), "INFO") + response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=False, + params={"id": device_id} + ) + if response: + self.log("Received API response for device id '{0}': {1}".format(device_id, str(response)), "DEBUG") + response = response.get("response") + if response: + device_ip = response[0]["managementIpAddress"] + if device_ip: + device_ip_mapping[device_id] = device_ip + self.log("Added device IP '{0}' for device id '{1}'.".format(device_ip, device_id), "INFO") + else: + device_ip_mapping[device_id] = None + self.log("No device ID found for device id '{0}'.".format(device_id), "WARNING") + else: + device_ip_mapping[device_id] = None + self.log("No response received for device id '{0}'.".format(device_id), "WARNING") + else: + device_ip_mapping[device_id] = None + self.log("No response received from 'get_device_list' for device id '{0}'.".format(device_id), "ERROR") + + except Exception as e: + error_message = "Exception occurred while fetching device ip for device id '{0}': {1}".format(device_id, str(e)) + self.log(error_message, "ERROR") + device_ip_mapping[device_id] = None + + self.log("Exiting 'get_device_ips_from_device_ids' with device IP mapping: '{0}'".format(device_ip_mapping), "INFO") + return device_ip_mapping + + def get_network_device_tag_id(self, tag_name): + """ + Retrieves the ID of a network device tag from the Cisco Catalyst Center based on the tag name. + Args: + self (object): An instance of the class used for interacting with Cisco Catalyst Center. + tag_name (str): The name of the tag whose ID is to be retrieved. + Returns: + str or None: The tag ID if found, or `None` if the tag is not available or an error occurs. + Description: + This function queries the Cisco Catalyst Center API to retrieve the ID of a tag by its name. + It sends a request to the 'get_tag' API endpoint with the specified `tag_name`. If the tag is found, + the function extracts and returns its `id`. If no tag is found or an error occurs during the API call, + it logs appropriate messages and returns `None`. + """ + + self.log("Entering 'get_network_device_tag_id' with tag_name: '{0}'".format(tag_name), "INFO") + device_tag_id = None + + try: + response = self.dnac._exec( + family="tag", + function='get_tag', + op_modifies=False, + params={"name": tag_name} + ) + if not response: + self.log("No response received from 'get_tag' for tag '{0}'.".format(tag_name), "WARNING") + return device_tag_id + + response_data = response.get("response") + if not response_data: + self.log("Unable to fetch the tag details for the tag '{0}'.".format(tag_name), "WARNING") + return device_tag_id + + self.log("Received API response from 'get_tag': {0}".format(str(response_data)), "DEBUG") + device_tag_id = response_data[0]["id"] + if device_tag_id: + self.log("Received the tag ID '{0}' for the tag: {1}".format(device_tag_id, tag_name), "INFO") + else: + self.log("Tag ID not found in the response for tag '{0}'.".format(tag_name), "WARNING") + + except Exception as e: + self.msg = ( + "Exception occurred while fetching tag id for the tag '{0} 'from " + "Cisco Catalyst Center: {1}" + ).format(tag_name, str(e)) + self.set_operation_result("failed", False, self.msg, "INFO").check_return_status() + + return device_tag_id + + def get_list_from_dict_values(self, dict_name): + """ + Extracts values from a dictionary and returns a list of non-None values. + + Args: + self (object): An instance of the class used for interacting with Cisco Catalyst Center. + dict_name (dict): The dictionary from which values are extracted. Each key-value pair is + checked, and non-None values are included in the returned list. + Returns: + list: A list containing all non-None values from the dictionary. + Description: + This function iterates over a given dictionary, checking each key-value pair. If the value + is `None`, it logs a debug message and skips that value. Otherwise, it appends the value to + a list. If an exception occurs during this process, it logs the exception message and handles + the operation result. + """ + + values_list = [] + for key, value in dict_name.items(): + try: + if value is None: + self.log("Value for the key {0} is None so not including in the list.".format(key), "DEBUG") + continue + else: + self.log("Fetch the value '{0}' for the key '{1}'".format(value, key), "DEBUG") + values_list.append(value) + except Exception as e: + self.msg = ( + "Exception occurred while fetching value for the key '{0} 'from " + "Cisco Catalyst Center: {1}" + ).format(key, str(e)) + self.set_operation_result("failed", False, self.msg, "INFO").check_return_status() + + return values_list + + def is_valid_ipv4(self, ip_address): + """ + Validates an IPv4 address. + 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 +1400,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 +1414,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 +1452,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 +1538,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 +1556,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 +1574,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 +1585,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 +1603,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. @@ -1255,7 +1613,7 @@ def get_task_details_by_id(self, task_id): Call the API 'get_task_details_by_id' to get the details along with the failure reason. Return the details. """ - # Need to handle exception + task_details = None try: response = self.dnac._exec( @@ -1282,7 +1640,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 +1658,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 +1681,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 +1714,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 +1734,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 +1792,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 +1811,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, + ) - Parameters: - api_family (str): The API family (e.g., "sda"). + # 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 + + # 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,35 +1933,42 @@ 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() + 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) @@ -1567,7 +1992,90 @@ 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 - time.sleep(self.params.get("dnac_task_poll_interval")) + # Wait for the specified poll interval before the next check + 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): + """ + 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() + 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.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 + + 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 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 + + 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 + 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): @@ -1579,7 +2087,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/accesspoint_workflow_manager.py b/plugins/modules/accesspoint_workflow_manager.py index 96431dfee2..2d48a9b6f1 100644 --- a/plugins/modules/accesspoint_workflow_manager.py +++ b/plugins/modules/accesspoint_workflow_manager.py @@ -2291,7 +2291,8 @@ def get_site_device(self, site_id, ap_mac_address, site_exist=None, current_site not found or if an error occurs during the API call, it returns False. """ try: - device_list = self.get_device_ids_from_site(site_id) + site_name = self.have.get("site_name_hierarchy", self.want.get("site_name")) + device_list = self.get_device_ids_from_site(site_name, site_id) if current_config.get("id") is not None and current_config.get("id") in device_list: self.log("Device with MAC address: {0} found in site: {1} Proceeding with ap_site updation." .format(ap_mac_address, site_id), "INFO") @@ -2301,8 +2302,8 @@ def get_site_device(self, site_id, ap_mac_address, site_exist=None, current_site return False except Exception as e: - self.log("Failed to execute the get_device_ids_from_site function '{}'\ - Error: {}".format(site_id, str(e)), "ERROR") + self.log("Failed to execute the get_device_ids_from_site function '{0}'\ + Error: {1}".format(site_id, str(e)), "ERROR") return False def verify_ap_provision(self, wlc_ip_address): diff --git a/plugins/modules/device_configs_backup_workflow_manager.py b/plugins/modules/device_configs_backup_workflow_manager.py index 3011ffc3b2..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 = self.get_device_ip_from_device_id(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) @@ -802,34 +798,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 +884,59 @@ 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": + 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") + 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) + 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: + 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 + 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( + 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/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py index 79f3a44a76..163a31b293 100644 --- a/plugins/modules/device_credential_workflow_manager.py +++ b/plugins/modules/device_credential_workflow_manager.py @@ -533,7 +533,7 @@ password: '12345' port: 443 - - name: Update global device credentials using id + - name: Update global device credentials cisco.dnac.device_credential_workflow_manager: dnac_host: "{{ dnac_host }}" dnac_port: "{{ dnac_port }}" @@ -552,15 +552,12 @@ username: cli1 password: '12345' enable_password: '12345' - id: '1b3777a0-09c2-488b-a64c-6f3c441e343' snmp_v2c_read: - description: SNMPv2c Read1 read_community: '123456' - id: '07c3c5fc-35e6-4c83-bdcb-6322d4bf7103' snmp_v2c_write: - description: SNMPv2c Write1 write_community: '123456' - id: '9900b521-0dfb-4bd1-b044-d31037c8def3' snmp_v3: - auth_password: '12345678' auth_type: SHA @@ -569,21 +566,18 @@ privacy_type: AES128 username: snmpV31 description: snmpV31 - id: '4d3fd6ca-8b01-4a41-9f77-d411a49c2830' https_read: - description: HTTP Read1 username: HTTP Read1 password: '12345' port: 443 - id: '4d3fd6ca-8b01-4a41-9f77-d411a34c2834' https_write: - description: HTTP_Write1 username: HTTP_Write1 password: '12345' port: 443 - id: '4d3fd6ca-8b01-4a41-9f77-d411a34c2688' - - name: Update multiple global device credentials using id + - name: Update multiple global device credentials cisco.dnac.device_credential_workflow_manager: dnac_host: "{{ dnac_host }}" dnac_port: "{{ dnac_port }}" @@ -602,26 +596,20 @@ username: cli1 password: '12345' enable_password: '12345' - id: '1b3777a0-09c2-488b-a64c-6f3c441e343' - description: CLI2 username: cli2 password: '12345' enable_password: '12345' - id: '1d3777a0-09c2-488b-a64c-6f3c441e368' snmp_v2c_read: - description: SNMPv2c Read1 read_community: '123456' - id: '07c3c5fc-35e6-4c83-bdcb-6322d4bf7103' - description: SNMPv2c Read2 read_community: '123458' - id: '08c3c5fc-35e6-4c83-bdcb-6322d4bf7200' snmp_v2c_write: - description: SNMPv2c write1 write_community: '123456' - id: '07c3c5fc-35e6-4c83-bdcb-6322d4bf7400' - description: SNMPv2c Write1 write_community: '123466' - id: '9900b521-0dfb-4bd1-b044-d31037c8def3' snmp_v3: - auth_password: '12345678' auth_type: SHA @@ -630,7 +618,6 @@ privacy_type: AES128 username: snmpV31 description: snmpV31 - id: '4d3fd6ca-8b01-4a41-9f77-d411a49c2830' - auth_password: '12345678' auth_type: SHA snmp_mode: AUTHPRIV @@ -638,29 +625,24 @@ privacy_type: AES128 username: snmpV32 description: snmpV32 - id: '4d3fd6ca-8b01-4a41-9f77-d411a49c2300' https_read: - description: HTTP Read1 username: HTTP Read1 password: '12345' port: 443 - id: '4d3fd6ca-8b01-4a41-9f77-d411a34c2500' - description: HTTP Read2 username: HTTP Read2 password: '12345' port: 443 - id: '4d3fd6ca-8b01-4a41-9f77-d411a34c2834' https_write: - description: HTTP_Write1 username: HTTP_Write1 password: '12345' port: 443 - id: '4d3fd6ca-8b01-4a41-9f77-d411a34c2834' - description: HTTP_Write2 username: HTTP_Write2 password: '12345' port: 443 - id: '4f3fd6ca-8b01-4a41-9f77-d411a34c2804' - name: Update global device credential name/description using old name and description. cisco.dnac.device_credential_workflow_manager: @@ -823,9 +805,9 @@ def __init__(self, module): super().__init__(module) self.result["response"] = [ { - "globalCredential": {}, - "assignCredential": {}, - "applyCredential": {} + "global_credential": {}, + "assign_credential": {}, + "apply_credential": {} } ] @@ -1029,101 +1011,105 @@ def get_global_credentials_params(self): return global_credentials - def get_cli_params(self, cliDetails): + def get_cli_params(self, cli_details): """ Format the CLI parameters for the CLI credential configuration in Cisco Catalyst Center. Parameters: - cliDetails (list of dict) - Cisco Catalyst Center details containing CLI Credentials. + cli_details (list of dict) - Cisco Catalyst Center details containing CLI Credentials. Returns: - cliCredential (list of dict) - Processed CLI credential data + cli_credential (list of dict) - Processed CLI credential data in the format suitable for the Cisco Catalyst Center config. """ - cliCredential = [] - for item in cliDetails: + cli_credential = [] + + for item in cli_details: if item is None: - cliCredential.append(None) + cli_credential.append(None) else: value = { "username": item.get("username"), "description": item.get("description"), "id": item.get("id") } - cliCredential.append(value) - return cliCredential + cli_credential.append(value) + return cli_credential - def get_snmpV2cRead_params(self, snmpV2cReadDetails): + def get_snmp_v2c_read_params(self, snmp_v2c_read_details): """ Format the snmp_v2c_read parameters for the snmp_v2c_read credential configuration in Cisco Catalyst Center. Parameters: - snmpV2cReadDetails (list of dict) - Cisco Catalyst Center + snmp_v2c_read_details (list of dict) - Cisco Catalyst Center Details containing snmp_v2c_read Credentials. Returns: - snmpV2cRead (list of dict) - Processed snmp_v2c_read credential + snmp_v2c_read (list of dict) - Processed snmp_v2c_read credential data in the format suitable for the Cisco Catalyst Center config. """ - snmpV2cRead = [] - for item in snmpV2cReadDetails: + snmp_v2c_read = [] + + for item in snmp_v2c_read_details: if item is None: - snmpV2cRead.append(None) + snmp_v2c_read.append(None) else: value = { "description": item.get("description"), "id": item.get("id") } - snmpV2cRead.append(value) - return snmpV2cRead + snmp_v2c_read.append(value) + return snmp_v2c_read - def get_snmpV2cWrite_params(self, snmpV2cWriteDetails): + def get_snmp_v2c_write_params(self, snmp_v2c_write_details): """ Format the snmp_v2c_write parameters for the snmp_v2c_write credential configuration in Cisco Catalyst Center. Parameters: - snmpV2cWriteDetails (list of dict) - Cisco Catalyst Center + snmp_v2c_write_details (list of dict) - Cisco Catalyst Center Details containing snmp_v2c_write Credentials. Returns: - snmpV2cWrite (list of dict) - Processed snmp_v2c_write credential + snmp_v2c_write (list of dict) - Processed snmp_v2c_write credential data in the format suitable for the Cisco Catalyst Center config. """ - snmpV2cWrite = [] - for item in snmpV2cWriteDetails: + snmp_v2c_write = [] + + for item in snmp_v2c_write_details: if item is None: - snmpV2cWrite.append(None) + snmp_v2c_write.append(None) else: value = { "description": item.get("description"), "id": item.get("id") } - snmpV2cWrite.append(value) - return snmpV2cWrite + snmp_v2c_write.append(value) + return snmp_v2c_write - def get_httpsRead_params(self, httpsReadDetails): + def get_https_read_params(self, https_read_details): """ Format the https_read parameters for the https_read credential configuration in Cisco Catalyst Center. Parameters: - httpsReadDetails (list of dict) - Cisco Catalyst Center + https_read_details (list of dict) - Cisco Catalyst Center Details containing https_read Credentials. Returns: - httpsRead (list of dict) - Processed https_read credential + https_read (list of dict) - Processed https_read credential data in the format suitable for the Cisco Catalyst Center config. """ - httpsRead = [] - for item in httpsReadDetails: + https_read = [] + + for item in https_read_details: if item is None: - httpsRead.append(None) + https_read.append(None) else: value = { "description": item.get("description"), @@ -1131,27 +1117,28 @@ def get_httpsRead_params(self, httpsReadDetails): "port": item.get("port"), "id": item.get("id") } - httpsRead.append(value) - return httpsRead + https_read.append(value) + return https_read - def get_httpsWrite_params(self, httpsWriteDetails): + def get_https_write_params(self, https_write_details): """ Format the https_write parameters for the https_write credential configuration in Cisco Catalyst Center. Parameters: - httpsWriteDetails (list of dict) - Cisco Catalyst Center + https_write_details (list of dict) - Cisco Catalyst Center Details containing https_write Credentials. Returns: - httpsWrite (list of dict) - Processed https_write credential + https_write (list of dict) - Processed https_write credential data in the format suitable for the Cisco Catalyst Center config. """ - httpsWrite = [] - for item in httpsWriteDetails: + https_write = [] + + for item in https_write_details: if item is None: - httpsWrite.append(None) + https_write.append(None) else: value = { "description": item.get("description"), @@ -1159,25 +1146,26 @@ def get_httpsWrite_params(self, httpsWriteDetails): "port": item.get("port"), "id": item.get("id") } - httpsWrite.append(value) - return httpsWrite + https_write.append(value) + return https_write - def get_snmpV3_params(self, snmpV3Details): + def get_snmp_v3_params(self, snmp_v3_details): """ Format the snmp_v3 parameters for the snmp_v3 credential configuration in Cisco Catalyst Center. Parameters: - snmpV3Details (list of dict) - Cisco Catalyst Center details containing snmp_v3 Credentials. + snmp_v3_details (list of dict) - Cisco Catalyst Center details containing snmp_v3 Credentials. Returns: - snmpV3 (list of dict) - Processed snmp_v3 credential + snmp_v3 (list of dict) - Processed snmp_v3 credential data in the format suitable for the Cisco Catalyst Center config. """ - snmpV3 = [] - for item in snmpV3Details: + snmp_v3 = [] + + for item in snmp_v3_details: if item is None: - snmpV3.append(None) + snmp_v3.append(None) else: value = { "username": item.get("username"), @@ -1192,416 +1180,432 @@ def get_snmpV3_params(self, snmpV3Details): "authType": item.get("authType"), "privacyType": item.get("privacyType") }) - snmpV3.append(value) - return snmpV3 + snmp_v3.append(value) + return snmp_v3 - def get_cli_credentials(self, CredentialDetails, global_credentials): + def get_cli_credentials(self, credential_details, global_credentials): """ Get the current CLI Credential from Cisco Catalyst Center based on the provided playbook details. Check this API using the check_return_status. Parameters: - CredentialDetails (dict) - Playbook details containing Global Device Credentials. + credential_details (dict) - Playbook details containing Global Device Credentials. global_credentials (dict) - All global device credentials details. Returns: - cliDetails (List) - The current CLI credentials. + cli_details (List) - The current CLI credentials. """ # playbook CLI Credential details - all_CLI = CredentialDetails.get("cli_credential") + all_cli = credential_details.get("cli_credential") # All CLI details from Cisco Catalyst Center - cli_details = global_credentials.get("cliCredential") + global_cli_details = global_credentials.get("cliCredential") + # Cisco Catalyst Center details for the CLI Credential given in the playbook - cliDetails = [] - if all_CLI and cli_details: - for cliCredential in all_CLI: - cliDetail = None - cliId = cliCredential.get("id") - if cliId: - cliDetail = get_dict_result(cli_details, "id", cliId) - if not cliDetail: + cli_details = [] + + if all_cli and global_cli_details: + for cli_credential in all_cli: + cli_detail = None + cli_id = cli_credential.get("id") + if cli_id: + cli_detail = get_dict_result(global_cli_details, "id", cli_id) + if not cli_detail: self.msg = "CLI credential ID is invalid" self.status = "failed" return self.check_return_status() - cliDescription = cliCredential.get("description") - cliUsername = cliCredential.get("username") - if cliDescription and cliUsername and (not cliDetail): - for item in cli_details: - if item.get("description") == cliDescription \ - and item.get("username") == cliUsername: - if cliDetail: + cli_description = cli_credential.get("description") + cli_username = cli_credential.get("username") + if cli_description and cli_username and (not cli_detail): + for item in global_cli_details: + if item.get("description") == cli_description \ + and item.get("username") == cli_username: + if cli_detail: self.msg = "There are multiple CLI credentials with the same description and username. " + \ "Kindly provide the ID for the global device credentials." self.status = "failed" return self.check_return_status() - cliDetail = item - - if not cliDetail: - cliOldDescription = cliCredential.get("old_description") - cliOldUsername = cliCredential.get("old_username") - if cliOldDescription and cliOldUsername and (not cliDetail): - for item in cli_details: - if item.get("description") == cliOldDescription \ - and item.get("username") == cliOldUsername: - if cliDetail: + cli_detail = item + + if not cli_detail: + cli_old_description = cli_credential.get("old_description") + cli_old_username = cli_credential.get("old_username") + if cli_old_description and cli_old_username and (not cli_detail): + for item in global_cli_details: + if item.get("description") == cli_old_description \ + and item.get("username") == cli_old_username: + if cli_detail: self.msg = "There are multiple CLI credentials with the same old_description and old_username. " + \ "Kindly provide the ID for the global device credentials." self.status = "failed" return self.check_return_status() - cliDetail = item - if not cliDetail: + cli_detail = item + if not cli_detail: self.msg = "CLI credential old_description or old_username is invalid" self.status = "failed" return self.check_return_status() - cliDetails.append(cliDetail) + cli_details.append(cli_detail) - return cliDetails + return cli_details - def get_snmpV2cRead_credentials(self, CredentialDetails, global_credentials): + def get_snmp_v2c_read_credentials(self, credential_details, global_credentials): """ Get the current snmp_v2c_read Credential from Cisco Catalyst Center based on the provided playbook details. Check this API using the check_return_status. Parameters: - CredentialDetails (dict) - Playbook details containing Global Device Credentials. + credential_details (dict) - Playbook details containing Global Device Credentials. global_credentials (dict) - All global device credentials details. Returns: - snmpV2cReadDetails (List) - The current snmp_v2c_read. + snmp_v2c_read_details (List) - The current snmp_v2c_read. """ # Playbook snmp_v2c_read Credential details - all_snmpV2cRead = CredentialDetails.get("snmp_v2c_read") + all_snmp_v2c_read = credential_details.get("snmp_v2c_read") # All snmp_v2c_read details from the Cisco Catalyst Center - snmpV2cRead_details = global_credentials.get("snmpV2cRead") + global_snmp_v2c_read_details = global_credentials.get("snmpV2cRead") # Cisco Catalyst Center details for the snmp_v2c_read Credential given in the playbook - snmpV2cReadDetails = [] - if all_snmpV2cRead and snmpV2cRead_details: - for snmpV2cReadCredential in all_snmpV2cRead: - snmpV2cReadDetail = None - snmpV2cReadId = snmpV2cReadCredential.get("id") - if snmpV2cReadId: - snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) - if not snmpV2cReadDetail: + snmp_v2c_read_details = [] + + if all_snmp_v2c_read and global_snmp_v2c_read_details: + for snmp_v2c_read_credential in all_snmp_v2c_read: + snmp_v2c_read_detail = None + snmp_v2c_read_id = snmp_v2c_read_credential.get("id") + if snmp_v2c_read_id: + snmp_v2c_read_detail = get_dict_result(global_snmp_v2c_read_details, "id", snmp_v2c_read_id) + if not snmp_v2c_read_detail: self.msg = "snmp_v2c_read credential ID is invalid" self.status = "failed" return self.check_return_status() - snmpV2cReadDescription = snmpV2cReadCredential.get("description") - if snmpV2cReadDescription and (not snmpV2cReadDetail): - snmpV2cReadDetail = get_dict_result( - snmpV2cRead_details, + snmp_v2c_read_description = snmp_v2c_read_credential.get("description") + if snmp_v2c_read_description and (not snmp_v2c_read_detail): + snmp_v2c_read_detail = get_dict_result( + global_snmp_v2c_read_details, "description", - snmpV2cReadDescription + snmp_v2c_read_description ) - if not snmpV2cReadDetail: - snmpV2cReadOldDescription = snmpV2cReadCredential.get("old_description") - if snmpV2cReadOldDescription and (not snmpV2cReadDetail): - snmpV2cReadDetail = get_dict_result( - snmpV2cRead_details, + if not snmp_v2c_read_detail: + snmp_v2c_read_old_description = snmp_v2c_read_credential.get("old_description") + if snmp_v2c_read_old_description and (not snmp_v2c_read_detail): + snmp_v2c_read_detail = get_dict_result( + global_snmp_v2c_read_details, "description", - snmpV2cReadOldDescription + snmp_v2c_read_old_description ) - if not snmpV2cReadDetail: + if not snmp_v2c_read_detail: self.msg = "snmp_v2c_read credential old_description is invalid" self.status = "failed" return self.check_return_status() - snmpV2cReadDetails.append(snmpV2cReadDetail) - return snmpV2cReadDetails + snmp_v2c_read_details.append(snmp_v2c_read_detail) + return snmp_v2c_read_details - def get_snmpV2cWrite_credentials(self, CredentialDetails, global_credentials): + def get_snmp_v2c_write_credentials(self, credential_details, global_credentials): """ Get the current snmp_v2c_write Credential from Cisco Catalyst Center based on the provided playbook details. Check this API using the check_return_status. Parameters: - CredentialDetails (dict) - Playbook details containing Global Device Credentials. + credential_details (dict) - Playbook details containing Global Device Credentials. global_credentials (dict) - All global device credentials details. Returns: - snmpV2cWriteDetails (List) - The current snmp_v2c_write. + snmp_v2c_write_details (List) - The current snmp_v2c_write. """ # Playbook snmp_v2c_write Credential details - all_snmpV2cWrite = CredentialDetails.get("snmp_v2c_write") + all_snmp_v2c_write = credential_details.get("snmp_v2c_write") # All snmp_v2c_write details from the Cisco Catalyst Center - snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") + global_snmp_v2c_write_details = global_credentials.get("snmpV2cWrite") # Cisco Catalyst Center details for the snmp_v2c_write Credential given in the playbook - snmpV2cWriteDetails = [] - if all_snmpV2cWrite and snmpV2cWrite_details: - for snmpV2cWriteCredential in all_snmpV2cWrite: - snmpV2cWriteDetail = None - snmpV2cWriteId = snmpV2cWriteCredential.get("id") - if snmpV2cWriteId: - snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) - if not snmpV2cWriteDetail: + snmp_v2c_write_details = [] + + if all_snmp_v2c_write and global_snmp_v2c_write_details: + for snmp_v2c_write_credential in all_snmp_v2c_write: + snmp_v2c_write_detail = None + snmp_v2c_write_id = snmp_v2c_write_credential.get("id") + if snmp_v2c_write_id: + snmp_v2c_write_detail = get_dict_result(global_snmp_v2c_write_details, "id", snmp_v2c_write_id) + if not snmp_v2c_write_detail: self.msg = "snmp_v2c_write credential ID is invalid" self.status = "failed" return self.check_return_status() - snmpV2cWriteDescription = snmpV2cWriteCredential.get("description") - if snmpV2cWriteDescription and (not snmpV2cWriteDetail): - snmpV2cWriteDetail = get_dict_result( - snmpV2cWrite_details, + snmp_v2c_write_description = snmp_v2c_write_credential.get("description") + + if snmp_v2c_write_description and (not snmp_v2c_write_detail): + snmp_v2c_write_detail = get_dict_result( + global_snmp_v2c_write_details, "description", - snmpV2cWriteDescription + snmp_v2c_write_description ) - if not snmpV2cWriteDetail: - snmpV2cWriteOldDescription = snmpV2cWriteCredential.get("old_description") - if snmpV2cWriteOldDescription and (not snmpV2cWriteDetail): - snmpV2cWriteDetail = get_dict_result( - snmpV2cWrite_details, + if not snmp_v2c_write_detail: + snmp_v2c_write_old_description = snmp_v2c_write_credential.get("old_description") + if snmp_v2c_write_old_description and (not snmp_v2c_write_detail): + snmp_v2c_write_detail = get_dict_result( + global_snmp_v2c_write_details, "description", - snmpV2cWriteOldDescription + snmp_v2c_write_old_description ) - if not snmpV2cWriteDetail: + if not snmp_v2c_write_detail: self.msg = "snmp_v2c_write credential old_description is invalid " self.status = "failed" return self.check_return_status() - snmpV2cWriteDetails.append(snmpV2cWriteDetail) - return snmpV2cWriteDetails + snmp_v2c_write_details.append(snmp_v2c_write_detail) + return snmp_v2c_write_details - def get_httpsRead_credentials(self, CredentialDetails, global_credentials): + def get_https_read_credentials(self, credential_details, global_credentials): """ Get the current https_read Credential from Cisco Catalyst Center based on the provided playbook details. Check this API using the check_return_status. Parameters: - CredentialDetails (dict) - Playbook details containing Global Device Credentials. + credential_details (dict) - Playbook details containing Global Device Credentials. global_credentials (dict) - All global device credentials details. Returns: - httpsReadDetails (List) - The current https_read. + https_read_details (List) - The current https_read. """ # Playbook https_read Credential details - all_httpsRead = CredentialDetails.get("https_read") + all_https_read = credential_details.get("https_read") # All https_read details from the Cisco Catalyst Center - httpsRead_details = global_credentials.get("httpsRead") + global_https_read_details = global_credentials.get("httpsRead") # Cisco Catalyst Center details for the https_read Credential given in the playbook - httpsReadDetails = [] - if all_httpsRead and httpsRead_details: - for httpsReadCredential in all_httpsRead: - httpsReadDetail = None - httpsReadId = httpsReadCredential.get("id") - if httpsReadId: - httpsReadDetail = get_dict_result(httpsRead_details, "id", httpsReadId) - if not httpsReadDetail: + https_read_details = [] + + if all_https_read and global_https_read_details: + for https_read_credential in all_https_read: + https_read__detail = None + https_read_id = https_read_credential.get("id") + if https_read_id: + https_read__detail = get_dict_result(global_https_read_details, "id", https_read_id) + if not https_read__detail: self.msg = "https_read credential Id is invalid" self.status = "failed" return self.check_return_status() - httpsReadDescription = httpsReadCredential.get("description") - httpsReadUsername = httpsReadCredential.get("username") - if httpsReadDescription and httpsReadUsername and (not httpsReadDetail): - for item in httpsRead_details: - if item.get("description") == httpsReadDescription \ - and item.get("username") == httpsReadUsername: - if httpsReadDetail: + https_read_description = https_read_credential.get("description") + https_read_username = https_read_credential.get("username") + + if https_read_description and https_read_username and (not https_read__detail): + for item in global_https_read_details: + if item.get("description") == https_read_description \ + and item.get("username") == https_read_username: + if https_read__detail: self.msg = "There are multiple https_read credentials with the same description and username. " + \ "Kindly provide the ID for the global device credentials." self.status = "failed" return self.check_return_status() - httpsReadDetail = item - - if not httpsReadDetail: - httpsReadOldDescription = httpsReadCredential.get("old_description") - httpsReadOldUsername = httpsReadCredential.get("old_username") - if httpsReadOldDescription and httpsReadOldUsername and (not httpsReadDetail): - for item in httpsRead_details: - if item.get("description") == httpsReadOldDescription \ - and item.get("username") == httpsReadOldUsername: - if httpsReadDetail: + https_read__detail = item + + if not https_read__detail: + https_read_old_description = https_read_credential.get("old_description") + https_read_old_username = https_read_credential.get("old_username") + if https_read_old_description and https_read_old_username and (not https_read__detail): + for item in global_https_read_details: + if item.get("description") == https_read_old_description \ + and item.get("username") == https_read_old_username: + if https_read__detail: self.msg = "There are multiple https_read credentials with the same old_description and old_username. " + \ "Kindly provide the ID for the global device credentials." self.status = "failed" return self.check_return_status() - httpsReadDetail = item - if not httpsReadDetail: + https_read__detail = item + if not https_read__detail: self.msg = "https_read credential old_description or old_username is invalid" self.status = "failed" return self.check_return_status() - httpsReadDetails.append(httpsReadDetail) - return httpsReadDetails + https_read_details.append(https_read__detail) + return https_read_details - def get_httpsWrite_credentials(self, CredentialDetails, global_credentials): + def get_https_write_credentials(self, credential_details, global_credentials): """ Get the current https_write Credential from Cisco Catalyst Center based on the provided playbook details. Check this API using the check_return_status. Parameters: - CredentialDetails (dict) - Playbook details containing Global Device Credentials. + credential_details (dict) - Playbook details containing Global Device Credentials. global_credentials (dict) - All global device credentials details. Returns: - httpsWriteDetails (List) - The current https_write. + https_write_details (List) - The current https_write. """ # Playbook https_write Credential details - all_httpsWrite = CredentialDetails.get("https_write") + all_https_write = credential_details.get("https_write") # All https_write details from the Cisco Catalyst Center - httpsWrite_details = global_credentials.get("httpsWrite") + global_https_write_details = global_credentials.get("httpsWrite") # Cisco Catalyst Center details for the https_write Credential given in the playbook - httpsWriteDetails = [] - if all_httpsWrite and httpsWrite_details: - for httpsWriteCredential in all_httpsWrite: - httpsWriteDetail = None - httpsWriteId = httpsWriteCredential.get("id") - if httpsWriteId: - httpsWriteDetail = get_dict_result(httpsWrite_details, "id", httpsWriteId) - if not httpsWriteDetail: + https_write_details = [] + + if all_https_write and global_https_write_details: + for https_write_credential in all_https_write: + https_write_detail = None + https_write_id = https_write_credential.get("id") + if https_write_id: + https_write_detail = get_dict_result(global_https_write_details, "id", https_write_id) + if not https_write_detail: self.msg = "https_write credential Id is invalid" self.status = "failed" return self.check_return_status() - httpsWriteDescription = httpsWriteCredential.get("description") - httpsWriteUsername = httpsWriteCredential.get("username") - if httpsWriteDescription and httpsWriteUsername and (not httpsWriteDetail): - for item in httpsWrite_details: - if item.get("description") == httpsWriteDescription \ - and item.get("username") == httpsWriteUsername: - if httpsWriteDetail: + https_write_description = https_write_credential.get("description") + https_write_username = https_write_credential.get("username") + if https_write_description and https_write_username and (not https_write_detail): + for item in global_https_write_details: + if item.get("description") == https_write_description \ + and item.get("username") == https_write_username: + if https_write_detail: self.msg = "There are multiple https_write credentials with the same description and username. " + \ "Kindly provide the ID for the global device credentials." self.status = "failed" return self.check_return_status() - httpsWriteDetail = item - - if not httpsWriteDetail: - httpsWriteOldDescription = httpsWriteCredential.get("old_description") - httpsWriteOldUsername = httpsWriteCredential.get("old_username") - if httpsWriteOldDescription and httpsWriteOldUsername and (not httpsWriteDetail): - for item in httpsWrite_details: - if item.get("description") == httpsWriteOldDescription \ - and item.get("username") == httpsWriteOldUsername: - if httpsWriteDetail: + https_write_detail = item + + if not https_write_detail: + https_write_old_description = https_write_credential.get("old_description") + https_write_old_username = https_write_credential.get("old_username") + if https_write_old_description and https_write_old_username and (not https_write_detail): + for item in global_https_write_details: + if item.get("description") == https_write_old_description \ + and item.get("username") == https_write_old_username: + if https_write_detail: self.msg = "There are multiple https_write credentials with the same old_description and old_username. " + \ "Kindly provide the ID for the global device credentials." self.status = "failed" return self.check_return_status() - httpsWriteDetail = item - if not httpsWriteDetail: + https_write_detail = item + + if not https_write_detail: self.msg = "https_write credential old_description or " + \ "old_username is invalid" self.status = "failed" return self.check_return_status() - httpsWriteDetails.append(httpsWriteDetail) - return httpsWriteDetails + https_write_details.append(https_write_detail) + return https_write_details - def get_snmpV3_credentials(self, CredentialDetails, global_credentials): + def get_snmp_v3_credentials(self, credential_details, global_credentials): """ Get the current snmp_v3 Credential from Cisco Catalyst Center based on the provided playbook details. Check this API using the check_return_status. Parameters: - CredentialDetails (dict) - Playbook details containing Global Device Credentials. + credential_details (dict) - Playbook details containing Global Device Credentials. global_credentials (dict) - All global device credentials details. Returns: - snmpV3Details (List) - The current snmp_v3. + snmp_v3_details (List) - The current snmp_v3. """ # Playbook snmp_v3 Credential details - all_snmpV3 = CredentialDetails.get("snmp_v3") + all_snmp_v3 = credential_details.get("snmp_v3") # All snmp_v3 details from the Cisco Catalyst Center - snmpV3_details = global_credentials.get("snmpV3") + global_snmp_v3_details = global_credentials.get("snmpV3") # Cisco Catalyst Center details for the snmp_v3 Credential given in the playbook - snmpV3Details = [] - if all_snmpV3 and snmpV3_details: - for snmpV3Credential in all_snmpV3: - snmpV3Detail = None - snmpV3Id = snmpV3Credential.get("id") - if snmpV3Id: - snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) - if not snmpV3Detail: + snmp_v3_details = [] + + if all_snmp_v3 and global_snmp_v3_details: + for snmp_v3_credential in all_snmp_v3: + snmp_v3_detail = None + snmp_v3_id = snmp_v3_credential.get("id") + if snmp_v3_id: + snmp_v3_detail = get_dict_result(global_snmp_v3_details, "id", snmp_v3_id) + if not snmp_v3_detail: self.msg = "snmp_v3 credential id is invalid" self.status = "failed" return self.check_return_status() - snmpV3Description = snmpV3Credential.get("description") - if snmpV3Description and (not snmpV3Detail): - snmpV3Detail = get_dict_result(snmpV3_details, "description", snmpV3Description) + snmp_v3_description = snmp_v3_credential.get("description") + + if snmp_v3_description and (not snmp_v3_detail): + snmp_v3_detail = get_dict_result(global_snmp_v3_details, "description", snmp_v3_description) - if not snmpV3Detail: - snmpV3OldDescription = snmpV3Credential.get("old_description") - if snmpV3OldDescription and (not snmpV3Detail): - snmpV3Detail = get_dict_result(snmpV3_details, "description", snmpV3OldDescription) - if not snmpV3Detail: + if not snmp_v3_detail: + snmp_v3_old_description = snmp_v3_credential.get("old_description") + if snmp_v3_old_description and (not snmp_v3_detail): + snmp_v3_detail = get_dict_result(global_snmp_v3_details, "description", snmp_v3_old_description) + if not snmp_v3_detail: self.msg = "snmp_v3 credential old_description is invalid" self.status = "failed" return self.check_return_status() - snmpV3Details.append(snmpV3Detail) - return snmpV3Details + snmp_v3_details.append(snmp_v3_detail) + return snmp_v3_details - def get_have_device_credentials(self, CredentialDetails): + def get_have_device_credentials(self, credential_details): """ Get the current Global Device Credentials from Cisco Catalyst Center based on the provided playbook details. Check this API using the check_return_status. Parameters: - CredentialDetails (dict) - Playbook details containing Global Device Credentials. + credential_details (dict) - Playbook details containing Global Device Credentials. Returns: self - The current object with updated information. """ global_credentials = self.get_global_credentials_params() - cliDetails = self.get_cli_credentials(CredentialDetails, global_credentials) - snmpV2cReadDetails = self.get_snmpV2cRead_credentials(CredentialDetails, global_credentials) - snmpV2cWriteDetails = self.get_snmpV2cWrite_credentials(CredentialDetails, - global_credentials) - httpsReadDetails = self.get_httpsRead_credentials(CredentialDetails, global_credentials) - httpsWriteDetails = self.get_httpsWrite_credentials(CredentialDetails, global_credentials) - snmpV3Details = self.get_snmpV3_credentials(CredentialDetails, global_credentials) - self.have.update({"globalCredential": {}}) - if cliDetails: - cliCredential = self.get_cli_params(cliDetails) - self.have.get("globalCredential").update({"cliCredential": cliCredential}) - if snmpV2cReadDetails: - snmpV2cRead = self.get_snmpV2cRead_params(snmpV2cReadDetails) - self.have.get("globalCredential").update({"snmpV2cRead": snmpV2cRead}) - if snmpV2cWriteDetails: - snmpV2cWrite = self.get_snmpV2cWrite_params(snmpV2cWriteDetails) - self.have.get("globalCredential").update({"snmpV2cWrite": snmpV2cWrite}) - if httpsReadDetails: - httpsRead = self.get_httpsRead_params(httpsReadDetails) - self.have.get("globalCredential").update({"httpsRead": httpsRead}) - if httpsWriteDetails: - httpsWrite = self.get_httpsWrite_params(httpsWriteDetails) - self.have.get("globalCredential").update({"httpsWrite": httpsWrite}) - if snmpV3Details: - snmpV3 = self.get_snmpV3_params(snmpV3Details) - self.have.get("globalCredential").update({"snmpV3": snmpV3}) + cli_details = self.get_cli_credentials(credential_details, global_credentials) + snmp_v2c_read_details = self.get_snmp_v2c_read_credentials(credential_details, global_credentials) + snmp_v2c_write_details = self.get_snmp_v2c_write_credentials(credential_details, global_credentials) + https_read_details = self.get_https_read_credentials(credential_details, global_credentials) + https_write_details = self.get_https_write_credentials(credential_details, global_credentials) + snmp_v3_details = self.get_snmp_v3_credentials(credential_details, global_credentials) + self.have.update({"global_credential": {}}) + + if cli_details: + cli_credential = self.get_cli_params(cli_details) + self.have.get("global_credential").update({"cliCredential": cli_credential}) + + if snmp_v2c_read_details: + snmp_v2c_read = self.get_snmp_v2c_read_params(snmp_v2c_read_details) + self.have.get("global_credential").update({"snmpV2cRead": snmp_v2c_read}) + + if snmp_v2c_write_details: + snmp_v2c_write = self.get_snmp_v2c_write_params(snmp_v2c_write_details) + self.have.get("global_credential").update({"snmpV2cWrite": snmp_v2c_write}) + + if https_read_details: + https_read = self.get_https_read_params(https_read_details) + self.have.get("global_credential").update({"httpsRead": https_read}) + + if https_write_details: + https_write = self.get_https_write_params(https_write_details) + self.have.get("global_credential").update({"httpsWrite": https_write}) + + if snmp_v3_details: + snmp_v3 = self.get_snmp_v3_params(snmp_v3_details) + self.have.get("global_credential").update({"snmpV3": snmp_v3}) self.log("Global device credential details: {0}" - .format(self.have.get("globalCredential")), "DEBUG") + .format(self.have.get("global_credential")), "DEBUG") self.msg = "Collected the Global Device Credential Details from the Cisco Catalyst Center" self.status = "success" return self @@ -1622,21 +1626,21 @@ def get_have(self, config): """ if config.get("global_credential_details") is not None: - CredentialDetails = config.get("global_credential_details") - self.get_have_device_credentials(CredentialDetails).check_return_status() + credential_details = config.get("global_credential_details") + self.get_have_device_credentials(credential_details).check_return_status() self.log("Current State (have): {0}".format(self.have), "INFO") self.msg = "Successfully retrieved the details from the Cisco Catalyst Center" self.status = "success" return self - def get_want_device_credentials(self, CredentialDetails): + def get_want_device_credentials(self, credential_details): """ Get the Global Device Credentials from the playbook. Check this API using the check_return_status. Parameters: - CredentialDetails (dict) - Playbook details containing Global Device Credentials. + credential_details (dict) - Playbook details containing Global Device Credentials. Returns: self - The current object with updated information of @@ -1647,15 +1651,17 @@ def get_want_device_credentials(self, CredentialDetails): "want_create": {}, "want_update": {} } - if CredentialDetails.get("cli_credential"): - cli = CredentialDetails.get("cli_credential") + + if credential_details.get("cli_credential"): + cli = credential_details.get("cli_credential") have_cli_ptr = 0 create_cli_ptr = 0 update_cli_ptr = 0 values = ["password", "description", "username", "id"] - have_cliCredential = self.have.get("globalCredential").get("cliCredential") + have_cli_credential = self.have.get("global_credential").get("cliCredential") + for item in cli: - if not have_cliCredential or have_cliCredential[have_cli_ptr] is None: + if not have_cli_credential or have_cli_credential[have_cli_ptr] is None: if want.get("want_create").get("cliCredential") is None: want.get("want_create").update({"cliCredential": []}) create_credential = want.get("want_create").get("cliCredential") @@ -1692,7 +1698,7 @@ def get_want_device_credentials(self, CredentialDetails): .update({values[i]: item.get(values[i])}) else: update_credential[update_cli_ptr].update({ - values[i]: self.have.get("globalCredential") + values[i]: self.have.get("global_credential") .get("cliCredential")[have_cli_ptr].get(values[i]) }) @@ -1703,16 +1709,17 @@ def get_want_device_credentials(self, CredentialDetails): update_cli_ptr = update_cli_ptr + 1 have_cli_ptr = have_cli_ptr + 1 - if CredentialDetails.get("snmp_v2c_read"): - snmpV2cRead = CredentialDetails.get("snmp_v2c_read") + if credential_details.get("snmp_v2c_read"): + snmp_v2c_read = credential_details.get("snmp_v2c_read") have_snmpv2cread_ptr = 0 create_snmpv2cread_ptr = 0 update_snmpv2cread_ptr = 0 values = ["read_community", "description", "id"] keys = ["readCommunity", "description", "id"] - have_snmpV2cRead = self.have.get("globalCredential").get("snmpV2cRead") - for item in snmpV2cRead: - if not have_snmpV2cRead or have_snmpV2cRead[have_snmpv2cread_ptr] is None: + have_snmp_v2c_read = self.have.get("global_credential").get("snmpV2cRead") + + for item in snmp_v2c_read: + if not have_snmp_v2c_read or have_snmp_v2c_read[have_snmpv2cread_ptr] is None: if want.get("want_create").get("snmpV2cRead") is None: want.get("want_create").update({"snmpV2cRead": []}) create_credential = want.get("want_create").get("snmpV2cRead") @@ -1744,22 +1751,23 @@ def get_want_device_credentials(self, CredentialDetails): .update({values[i]: item.get(values[i])}) else: update_credential[update_snmpv2cread_ptr].update({ - values[i]: self.have.get("globalCredential") + values[i]: self.have.get("global_credential") .get("snmpV2cRead")[have_snmpv2cread_ptr].get(values[i]) }) update_snmpv2cread_ptr = update_snmpv2cread_ptr + 1 have_snmpv2cread_ptr = have_snmpv2cread_ptr + 1 - if CredentialDetails.get("snmp_v2c_write"): - snmpV2cWrite = CredentialDetails.get("snmp_v2c_write") + if credential_details.get("snmp_v2c_write"): + snmp_v2c_write = credential_details.get("snmp_v2c_write") have_snmpv2cwrite_ptr = 0 create_snmpv2cwrite_ptr = 0 update_snmpv2cwrite_ptr = 0 values = ["write_community", "description", "id"] keys = ["writeCommunity", "description", "id"] - have_snmpV2cWrite = self.have.get("globalCredential").get("snmpV2cWrite") - for item in snmpV2cWrite: - if not have_snmpV2cWrite or have_snmpV2cWrite[have_snmpv2cwrite_ptr] is None: + have_snmp_v2c_write = self.have.get("global_credential").get("snmpV2cWrite") + + for item in snmp_v2c_write: + if not have_snmp_v2c_write or have_snmp_v2c_write[have_snmpv2cwrite_ptr] is None: if want.get("want_create").get("snmpV2cWrite") is None: want.get("want_create").update({"snmpV2cWrite": []}) create_credential = want.get("want_create").get("snmpV2cWrite") @@ -1791,23 +1799,24 @@ def get_want_device_credentials(self, CredentialDetails): .update({values[i]: item.get(values[i])}) else: update_credential[update_snmpv2cwrite_ptr].update({ - values[i]: self.have.get("globalCredential") + values[i]: self.have.get("global_credential") .get("snmpV2cWrite")[have_snmpv2cwrite_ptr].get(values[i]) }) update_snmpv2cwrite_ptr = update_snmpv2cwrite_ptr + 1 have_snmpv2cwrite_ptr = have_snmpv2cwrite_ptr + 1 - if CredentialDetails.get("https_read"): - httpsRead = CredentialDetails.get("https_read") + if credential_details.get("https_read"): + https_read = credential_details.get("https_read") have_httpsread_ptr = 0 create_httpsread_ptr = 0 update_httpsread_ptr = 0 values = ["password", "description", "username", "id", "port"] - have_httpsRead = self.have.get("globalCredential").get("httpsRead") - for item in httpsRead: + have_https_read = self.have.get("global_credential").get("httpsRead") + + for item in https_read: self.log("Global credentials details: {0}" - .format(self.have.get("globalCredential")), "DEBUG") - if not have_httpsRead or have_httpsRead[have_httpsread_ptr] is None: + .format(self.have.get("global_credential")), "DEBUG") + if not have_https_read or have_https_read[have_httpsread_ptr] is None: if want.get("want_create").get("httpsRead") is None: want.get("want_create").update({"httpsRead": []}) create_credential = want.get("want_create").get("httpsRead") @@ -1845,21 +1854,22 @@ def get_want_device_credentials(self, CredentialDetails): .update({values[i]: item.get(values[i])}) else: update_credential[update_httpsread_ptr].update({ - values[i]: self.have.get("globalCredential") + values[i]: self.have.get("global_credential") .get("httpsRead")[have_httpsread_ptr].get(values[i]) }) update_httpsread_ptr = update_httpsread_ptr + 1 have_httpsread_ptr = have_httpsread_ptr + 1 - if CredentialDetails.get("https_write"): - httpsWrite = CredentialDetails.get("https_write") + if credential_details.get("https_write"): + https_write = credential_details.get("https_write") have_httpswrite_ptr = 0 create_httpswrite_ptr = 0 update_httpswrite_ptr = 0 values = ["password", "description", "username", "id", "port"] - have_httpsWrite = self.have.get("globalCredential").get("httpsWrite") - for item in httpsWrite: - if not have_httpsWrite or have_httpsWrite[have_httpswrite_ptr] is None: + have_https_write = self.have.get("global_credential").get("httpsWrite") + + for item in https_write: + if not have_https_write or have_https_write[have_httpswrite_ptr] is None: if want.get("want_create").get("httpsWrite") is None: want.get("want_create").update({"httpsWrite": []}) create_credential = want.get("want_create").get("httpsWrite") @@ -1897,21 +1907,22 @@ def get_want_device_credentials(self, CredentialDetails): .update({values[i]: item.get(values[i])}) else: update_credential[update_httpswrite_ptr].update({ - values[i]: self.have.get("globalCredential") + values[i]: self.have.get("global_credential") .get("httpsWrite")[have_httpswrite_ptr].get(values[i]) }) update_httpswrite_ptr = update_httpswrite_ptr + 1 have_httpswrite_ptr = have_httpswrite_ptr + 1 - if CredentialDetails.get("snmp_v3"): - snmpV3 = CredentialDetails.get("snmp_v3") + if credential_details.get("snmp_v3"): + snmp_v3 = credential_details.get("snmp_v3") have_snmpv3_ptr = 0 create_snmpv3_ptr = 0 update_snmpv3_ptr = 0 values = ["description", "username", "id"] - have_snmpV3 = self.have.get("globalCredential").get("snmpV3") - for item in snmpV3: - if not have_snmpV3 or have_snmpV3[have_snmpv3_ptr] is None: + have_snmp_v3 = self.have.get("global_credential").get("snmpV3") + + for item in snmp_v3: + if not have_snmp_v3 or have_snmp_v3[have_snmpv3_ptr] is None: if want.get("want_create").get("snmpV3") is None: want.get("want_create").update({"snmpV3": []}) create_credential = want.get("want_create").get("snmpV3") @@ -1985,7 +1996,7 @@ def get_want_device_credentials(self, CredentialDetails): .update({value: item.get(value)}) else: update_credential[update_snmpv3_ptr].update({ - value: self.have.get("globalCredential") + value: self.have.get("global_credential") .get("snmpV3")[have_snmpv3_ptr].get(value) }) if item.get("snmp_mode"): @@ -1996,10 +2007,10 @@ def get_want_device_credentials(self, CredentialDetails): if item.get("auth_type"): update_credential[update_snmpv3_ptr] \ .update({"authType": item.get("auth_type")}) - elif self.have.get("globalCredential") \ + elif self.have.get("global_credential") \ .get("snmpMode")[have_snmpv3_ptr].get("authType"): update_credential[update_snmpv3_ptr].update({ - "authType": self.have.get("globalCredential") + "authType": self.have.get("global_credential") .get("snmpMode")[have_snmpv3_ptr].get("authType") }) else: @@ -2023,10 +2034,10 @@ def get_want_device_credentials(self, CredentialDetails): if item.get("privacy_type"): update_credential[update_snmpv3_ptr] \ .update({"privacyType": item.get("privacy_type")}) - elif self.have.get("globalCredential") \ + elif self.have.get("global_credential") \ .get("snmpMode")[have_snmpv3_ptr].get("privacyType"): update_credential[update_snmpv3_ptr].update({ - "privacyType": self.have.get("globalCredential") + "privacyType": self.have.get("global_credential") .get("snmpMode")[have_snmpv3_ptr].get("privacyType") }) else: @@ -2053,13 +2064,13 @@ def get_want_device_credentials(self, CredentialDetails): self.status = "success" return self - def get_want_assign_credentials(self, AssignCredentials): + def get_want_assign_credentials(self, assign_credentials): """ Get the Credentials to be assigned to a site from the playbook. Check this API using the check_return_status. Parameters: - AssignCredentials (dict) - Playbook details containing + assign_credentials (dict) - Playbook details containing credentials that need to be assigned to a site. Returns: @@ -2069,8 +2080,10 @@ def get_want_assign_credentials(self, AssignCredentials): want = { "assign_credentials": {} } + current_ccc_version_as_int = self.get_ccc_version_as_integer() - site_names = AssignCredentials.get("site_name") + site_names = assign_credentials.get("site_name") + if not site_names: self.msg = "The 'site_name' is required parameter for 'assign_credentials_to_site'" self.status = "failed" @@ -2087,214 +2100,232 @@ def get_want_assign_credentials(self, AssignCredentials): want.update({"site_id": site_ids}) global_credentials = self.get_global_credentials_params() - cli_credential = AssignCredentials.get("cli_credential") + cli_credential = assign_credentials.get("cli_credential") + if cli_credential: - cliId = cli_credential.get("id") - cliDescription = cli_credential.get("description") - cliUsername = cli_credential.get("username") + cli_id = cli_credential.get("id") + cli_description = cli_credential.get("description") + cli_username = cli_credential.get("username") - if cliId or cliDescription and cliUsername: + if cli_id or cli_description and cli_username: # All CLI details from the Cisco Catalyst Center - cli_details = global_credentials.get("cliCredential") - if not cli_details: + global_cli_details = global_credentials.get("cliCredential") + + if not global_cli_details: self.msg = "Global CLI credential is not available" self.status = "failed" return self - cliDetail = None - if cliId: - cliDetail = get_dict_result(cli_details, "id", cliId) - if not cliDetail: + cli_detail = None + + if cli_id: + cli_detail = get_dict_result(global_cli_details, "id", cli_id) + if not cli_detail: self.msg = "The ID for the CLI credential is not valid." self.status = "failed" return self - elif cliDescription and cliUsername: - for item in cli_details: - if item.get("description") == cliDescription and \ - item.get("username") == cliUsername: - cliDetail = item - if not cliDetail: + elif cli_description and cli_username: + for item in global_cli_details: + if item.get("description") == cli_description and \ + item.get("username") == cli_username: + global_cli_details = item + if not global_cli_details: self.msg = "The username and description of the CLI credential are invalid" self.status = "failed" return self + if current_ccc_version_as_int <= self.get_ccc_version_as_int_from_str("2.3.5.3"): - want.get("assign_credentials").update({"cliId": cliDetail.get("id")}) + want.get("assign_credentials").update({"cliId": global_cli_details.get("id")}) else: want.get("assign_credentials").update({ - "cliCredentialsId": {"credentialsId": cliDetail.get("id")} + "cliCredentialsId": {"credentialsId": global_cli_details.get("id")} }) - snmp_v2c_read = AssignCredentials.get("snmp_v2c_read") + snmp_v2c_read = assign_credentials.get("snmp_v2c_read") if snmp_v2c_read: - snmpV2cReadId = snmp_v2c_read.get("id") - snmpV2cReadDescription = snmp_v2c_read.get("description") - if snmpV2cReadId or snmpV2cReadDescription: + snmp_v2c_read_id = snmp_v2c_read.get("id") + snmp_v2c_read_description = snmp_v2c_read.get("description") + if snmp_v2c_read_id or snmp_v2c_read_description: # All snmp_v2c_read details from the Cisco Catalyst Center - snmpV2cRead_details = global_credentials.get("snmpV2cRead") - if not snmpV2cRead_details: + global_snmp_v2c_read_details = global_credentials.get("snmpV2cRead") + if not global_snmp_v2c_read_details: self.msg = "Global snmp_v2c_read credential is not available" self.status = "failed" return self - snmpV2cReadDetail = None - if snmpV2cReadId: - snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) - if not snmpV2cReadDetail: + snmp_v2c_read_detail = None + + if snmp_v2c_read_id: + snmp_v2c_read_detail = get_dict_result(global_snmp_v2c_read_details, "id", snmp_v2c_read_id) + if not snmp_v2c_read_detail: self.msg = "The ID of the snmp_v2c_read credential is not valid." self.status = "failed" return self - elif snmpV2cReadDescription: - for item in snmpV2cRead_details: - if item.get("description") == snmpV2cReadDescription: - snmpV2cReadDetail = item - if not snmpV2cReadDetail: + elif snmp_v2c_read_description: + for item in global_snmp_v2c_read_details: + if item.get("description") == snmp_v2c_read_description: + snmp_v2c_read_detail = item + if not snmp_v2c_read_detail: self.msg = "The username and description for the snmp_v2c_read credential are invalid." self.status = "failed" return self + if current_ccc_version_as_int <= self.get_ccc_version_as_int_from_str("2.3.5.3"): - want.get("assign_credentials").update({"snmpV2ReadId": snmpV2cReadDetail.get("id")}) + want.get("assign_credentials").update({"snmpV2ReadId": snmp_v2c_read_detail.get("id")}) else: want.get("assign_credentials").update({ - "snmpv2cReadCredentialsId": {"credentialsId": snmpV2cReadDetail.get("id")} + "snmpv2cReadCredentialsId": {"credentialsId": snmp_v2c_read_detail.get("id")} }) - snmp_v2c_write = AssignCredentials.get("snmp_v2c_write") + snmp_v2c_write = assign_credentials.get("snmp_v2c_write") if snmp_v2c_write: - snmpV2cWriteId = snmp_v2c_write.get("id") - snmpV2cWriteDescription = snmp_v2c_write.get("description") - if snmpV2cWriteId or snmpV2cWriteDescription: - + snmp_v2c_write_id = snmp_v2c_write.get("id") + snmp_v2c_write_description = snmp_v2c_write.get("description") + if snmp_v2c_write_id or snmp_v2c_write_description: # All snmp_v2c_write details from the Cisco Catalyst Center - snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") - if not snmpV2cWrite_details: + global_snmp_v2c_write_details = global_credentials.get("snmpV2cWrite") + + if not global_snmp_v2c_write_details: self.msg = "Global snmp_v2c_write Credential is not available" self.status = "failed" return self - snmpV2cWriteDetail = None - if snmpV2cWriteId: - snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) - if not snmpV2cWriteDetail: + snmp_v2c_write_detail = None + + if snmp_v2c_write_id: + snmp_v2c_write_detail = get_dict_result(global_snmp_v2c_write_details, "id", snmp_v2c_write_id) + if not snmp_v2c_write_detail: self.msg = "The ID of the snmp_v2c_write credential is invalid." self.status = "failed" return self - elif snmpV2cWriteDescription: - for item in snmpV2cWrite_details: - if item.get("description") == snmpV2cWriteDescription: - snmpV2cWriteDetail = item - if not snmpV2cWriteDetail: + elif snmp_v2c_write_description: + for item in global_snmp_v2c_write_details: + if item.get("description") == snmp_v2c_write_description: + snmp_v2c_write_detail = item + + if not snmp_v2c_write_detail: self.msg = "The username and description of the snmp_v2c_write credential are invalid." self.status = "failed" return self + if current_ccc_version_as_int <= self.get_ccc_version_as_int_from_str("2.3.5.3"): - want.get("assign_credentials").update({"snmpV2WriteId": snmpV2cWriteDetail.get("id")}) + want.get("assign_credentials").update({"snmpV2WriteId": snmp_v2c_write_detail.get("id")}) else: want.get("assign_credentials").update({ - "snmpv2cWriteCredentialsId": {"credentialsId": snmpV2cWriteDetail.get("id")} + "snmpv2cWriteCredentialsId": {"credentialsId": snmp_v2c_write_detail.get("id")} }) - https_read = AssignCredentials.get("https_read") + https_read = assign_credentials.get("https_read") if https_read: - httpReadId = https_read.get("id") - httpReadDescription = https_read.get("description") - httpReadUsername = https_read.get("username") - if httpReadId or httpReadDescription and httpReadUsername: + https_read_id = https_read.get("id") + https_read_description = https_read.get("description") + https_read_username = https_read.get("username") + if https_read_id or https_read_description and https_read_username: # All httpRead details from the Cisco Catalyst Center - httpRead_details = global_credentials.get("httpsRead") - if not httpRead_details: - self.msg = "Global httpRead Credential is not available." + global_https_read_details = global_credentials.get("httpsRead") + if not global_https_read_details: + self.msg = "Global https_read Credential is not available." self.status = "failed" return self - httpReadDetail = None - if httpReadId: - httpReadDetail = get_dict_result(httpRead_details, "id", httpReadId) - if not httpReadDetail: - self.msg = "The ID of the httpRead credential is not valid." + https_read_detail = None + + if https_read_id: + https_read_detail = get_dict_result(global_https_read_details, "id", https_read_id) + if not https_read_detail: + self.msg = "The ID of the https_read credential is not valid." self.status = "failed" return self - elif httpReadDescription and httpReadUsername: - for item in httpRead_details: - if item.get("description") == httpReadDescription and \ - item.get("username") == httpReadUsername: - httpReadDetail = item - if not httpReadDetail: - self.msg = "The description and username for the httpRead credential are invalid." + elif https_read_description and https_read_username: + for item in global_https_read_details: + if item.get("description") == https_read_description and \ + item.get("username") == https_read_username: + https_read_detail = item + + if not https_read_detail: + self.msg = "The description and username for the https_read credential are invalid." self.status = "failed" return self + if current_ccc_version_as_int <= self.get_ccc_version_as_int_from_str("2.3.5.3"): - want.get("assign_credentials").update({"httpRead": httpReadDetail.get("id")}) + want.get("assign_credentials").update({"httpRead": https_read_detail.get("id")}) else: want.get("assign_credentials").update({ - "httpReadCredentialsId": {"credentialsId": httpReadDetail.get("id")} + "httpReadCredentialsId": {"credentialsId": https_read_detail.get("id")} }) - https_write = AssignCredentials.get("https_write") + https_write = assign_credentials.get("https_write") if https_write: - httpWriteId = https_write.get("id") - httpWriteDescription = https_write.get("description") - httpWriteUsername = https_write.get("username") - if httpWriteId or httpWriteDescription and httpWriteUsername: + https_write_id = https_write.get("id") + https_write_description = https_write.get("description") + https_write_username = https_write.get("username") + if https_write_id or https_write_description and https_write_username: # All httpWrite details from the Cisco Catalyst Center - httpWrite_details = global_credentials.get("httpsWrite") - if not httpWrite_details: - self.msg = "Global httpWrite credential is not available." + global_https_write_details = global_credentials.get("httpsWrite") + if not global_https_write_details: + self.msg = "Global https_write credential is not available." self.status = "failed" return self - httpWriteDetail = None - if httpWriteId: - httpWriteDetail = get_dict_result(httpWrite_details, "id", httpWriteId) - if not httpWriteDetail: - self.msg = "The ID of the httpWrite credential is not valid." + https_write_detail = None + + if https_write_id: + https_write_detail = get_dict_result(global_https_write_details, "id", https_write_id) + if not https_write_detail: + self.msg = "The ID of the https_write credential is not valid." self.status = "failed" return self - elif httpWriteDescription and httpWriteUsername: - for item in httpWrite_details: - if item.get("description") == httpWriteDescription and \ - item.get("username") == httpWriteUsername: - httpWriteDetail = item - if not httpWriteDetail: - self.msg = "The description and username for the httpWrite credential are invalid." + elif https_write_description and https_write_username: + for item in global_https_write_details: + if item.get("description") == https_write_description and \ + item.get("username") == https_write_username: + https_write_detail = item + + if not https_write_detail: + self.msg = "The description and username for the https_write credential are invalid." self.status = "failed" return self + if current_ccc_version_as_int <= self.get_ccc_version_as_int_from_str("2.3.5.3"): - want.get("assign_credentials").update({"httpWrite": httpWriteDetail.get("id")}) + want.get("assign_credentials").update({"httpWrite": https_write_detail.get("id")}) else: want.get("assign_credentials").update({ - "httpWriteCredentialsId": {"credentialsId": httpWriteDetail.get("id")} + "httpWriteCredentialsId": {"credentialsId": https_write_detail.get("id")} }) - snmp_v3 = AssignCredentials.get("snmp_v3") + snmp_v3 = assign_credentials.get("snmp_v3") if snmp_v3: - snmpV3Id = snmp_v3.get("id") - snmpV3Description = snmp_v3.get("description") - if snmpV3Id or snmpV3Description: - + snmp_v3_id = snmp_v3.get("id") + snmp_v3_description = snmp_v3.get("description") + if snmp_v3_id or snmp_v3_description: # All snmp_v3 details from the Cisco Catalyst Center - snmpV3_details = global_credentials.get("snmpV3") - if not snmpV3_details: + global_snmp_v3_details = global_credentials.get("snmpV3") + + if not global_snmp_v3_details: self.msg = "Global snmp_v3 Credential is not available." self.status = "failed" return self - snmpV3Detail = None - if snmpV3Id: - snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) - if not snmpV3Detail: + snmp_v3_detail = None + + if snmp_v3_id: + snmp_v3_detail = get_dict_result(global_snmp_v3_details, "id", snmp_v3_id) + if not snmp_v3_detail: self.msg = "The ID of the snmp_v3 credential is not valid." self.status = "failed" return self - elif snmpV3Description: - for item in snmpV3_details: - if item.get("description") == snmpV3Description: - snmpV3Detail = item - if not snmpV3Detail: + elif snmp_v3_description: + for item in global_snmp_v3_details: + if item.get("description") == snmp_v3_description: + snmp_v3_detail = item + + if not snmp_v3_detail: self.msg = "The username and description for the snmp_v2c_write credential are invalid." self.status = "failed" return self + if current_ccc_version_as_int <= self.get_ccc_version_as_int_from_str("2.3.5.3"): - want.get("assign_credentials").update({"snmpV3Id": snmpV3Detail.get("id")}) + want.get("assign_credentials").update({"snmpV3Id": snmp_v3_detail.get("id")}) else: want.get("assign_credentials").update({ - "snmpv3CredentialsId": {"credentialsId": snmpV3Detail.get("id")} + "snmpv3CredentialsId": {"credentialsId": snmp_v3_detail.get("id")} }) self.log("Desired State (want): {0}".format(want), "INFO") @@ -2303,13 +2334,13 @@ def get_want_assign_credentials(self, AssignCredentials): self.status = "success" return self - def get_want_apply_credentials(self, ApplyCredentials): + def get_want_apply_credentials(self, apply_credentials): """ Get the Credentials to be applied to a site from the playbook. Check this API using the check_return_status. Parameters: - ApplyCredentials (dict) - Playbook details containing + apply_credentials (dict) - Playbook details containing credentials that need to be applied to a site. Returns: @@ -2319,7 +2350,8 @@ def get_want_apply_credentials(self, ApplyCredentials): want = { "apply_credentials": {} } - site_names = ApplyCredentials.get("site_name") + + site_names = apply_credentials.get("site_name") if not site_names: self.msg = "The 'site_name' is required parameter for 'apply_credentials_to_site'" self.status = "failed" @@ -2337,129 +2369,140 @@ def get_want_apply_credentials(self, ApplyCredentials): want.update({"site_id": site_ids}) want.update({"site_name": site_names}) global_credentials = self.get_global_credentials_params() - cli_credential = ApplyCredentials.get("cli_credential") - if cli_credential: - cliId = cli_credential.get("id") - cliDescription = cli_credential.get("description") - cliUsername = cli_credential.get("username") + cli_credential = apply_credentials.get("cli_credential") - if cliId or cliDescription and cliUsername: + if cli_credential: + cli_id = cli_credential.get("id") + cli_description = cli_credential.get("description") + cli_username = cli_credential.get("username") + if cli_id or cli_description and cli_username: # All CLI details from the Cisco Catalyst Center - cli_details = global_credentials.get("cliCredential") - if not cli_details: + global_cli_details = global_credentials.get("cliCredential") + + if not global_cli_details: self.msg = "Global CLI credential is not available" self.status = "failed" return self - cliDetail = None - if cliId: - cliDetail = get_dict_result(cli_details, "id", cliId) - if not cliDetail: + cli_detail = None + + if cli_id: + cli_detail = get_dict_result(global_cli_details, "id", cli_id) + if not cli_detail: self.msg = "The ID for the CLI credential is not valid." self.status = "failed" return self - elif cliDescription and cliUsername: - for item in cli_details: - if item.get("description") == cliDescription and \ - item.get("username") == cliUsername: - cliDetail = item - if not cliDetail: + elif cli_description and cli_username: + for item in global_cli_details: + if item.get("description") == cli_description and \ + item.get("username") == cli_username: + cli_detail = item + if not cli_detail: self.msg = "The username and description of the CLI credential are invalid" self.status = "failed" return self - want["apply_credentials"]["cliId"] = cliDetail.get("id") + want["apply_credentials"]["cliId"] = cli_detail.get("id") + + snmp_v2c_read = apply_credentials.get("snmp_v2c_read") - snmp_v2c_read = ApplyCredentials.get("snmp_v2c_read") if snmp_v2c_read: - snmpV2cReadId = snmp_v2c_read.get("id") - snmpV2cReadDescription = snmp_v2c_read.get("description") - if snmpV2cReadId or snmpV2cReadDescription: + snmp_v2c_read_id = snmp_v2c_read.get("id") + snmp_v2c_read_description = snmp_v2c_read.get("description") + if snmp_v2c_read_id or snmp_v2c_read_description: # All snmp_v2c_read details from the Cisco Catalyst Center - snmpV2cRead_details = global_credentials.get("snmpV2cRead") - if not snmpV2cRead_details: + global_snmp_v2c_read_details = global_credentials.get("snmpV2cRead") + + if not global_snmp_v2c_read_details: self.msg = "Global snmp_v2c_read credential is not available" self.status = "failed" return self - snmpV2cReadDetail = None - if snmpV2cReadId: - snmpV2cReadDetail = get_dict_result( - snmpV2cRead_details, "id", snmpV2cReadId) - if not snmpV2cReadDetail: + snmp_v2c_read_detail = None + + if snmp_v2c_read_id: + snmp_v2c_read_detail = get_dict_result( + global_snmp_v2c_read_details, "id", snmp_v2c_read_id) + if not snmp_v2c_read_detail: self.msg = "The ID of the snmp_v2c_read credential is not valid." self.status = "failed" return self - elif snmpV2cReadDescription: - for item in snmpV2cRead_details: - if item.get("description") == snmpV2cReadDescription: - snmpV2cReadDetail = item - if not snmpV2cReadDetail: + elif snmp_v2c_read_description: + for item in global_snmp_v2c_read_details: + if item.get("description") == snmp_v2c_read_description: + snmp_v2c_read_detail = item + if not snmp_v2c_read_detail: self.msg = "The username and description for the snmp_v2c_read credential are invalid." self.status = "failed" return self - want["apply_credentials"]["snmpV2ReadId"] = snmpV2cReadDetail.get( + want["apply_credentials"]["snmpV2ReadId"] = snmp_v2c_read_detail.get( "id") - snmp_v2c_write = ApplyCredentials.get("snmp_v2c_write") + snmp_v2c_write = apply_credentials.get("snmp_v2c_write") + if snmp_v2c_write: - snmpV2cWriteId = snmp_v2c_write.get("id") - snmpV2cWriteDescription = snmp_v2c_write.get("description") - if snmpV2cWriteId or snmpV2cWriteDescription: + snmp_v2c_write_id = snmp_v2c_write.get("id") + snmp_v2c_write_description = snmp_v2c_write.get("description") + if snmp_v2c_write_id or snmp_v2c_write_description: # All snmp_v2c_write details from the Cisco Catalyst Center - snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") - if not snmpV2cWrite_details: + global_snmp_v2c_write_details = global_credentials.get("snmpV2cWrite") + + if not global_snmp_v2c_write_details: self.msg = "Global snmp_v2c_write Credential is not available" self.status = "failed" return self - snmpV2cWriteDetail = None - if snmpV2cWriteId: - snmpV2cWriteDetail = get_dict_result( - snmpV2cWrite_details, "id", snmpV2cWriteId) - if not snmpV2cWriteDetail: + snmp_v2c_write_detail = None + + if snmp_v2c_write_id: + snmp_v2c_write_detail = get_dict_result( + global_snmp_v2c_write_details, "id", snmp_v2c_write_id) + if not snmp_v2c_write_detail: self.msg = "The ID of the snmp_v2c_write credential is invalid." self.status = "failed" return self - elif snmpV2cWriteDescription: - for item in snmpV2cWrite_details: - if item.get("description") == snmpV2cWriteDescription: - snmpV2cWriteDetail = item - if not snmpV2cWriteDetail: + elif snmp_v2c_write_description: + for item in global_snmp_v2c_write_details: + if item.get("description") == snmp_v2c_write_description: + snmp_v2c_write_detail = item + if not snmp_v2c_write_detail: self.msg = "The username and description of the snmp_v2c_write credential are invalid." self.status = "failed" return self - want["apply_credentials"]["snmpV2WriteId"] = snmpV2cWriteDetail.get( + want["apply_credentials"]["snmpV2WriteId"] = snmp_v2c_write_detail.get( "id") - snmp_v3 = ApplyCredentials.get("snmp_v3") + snmp_v3 = apply_credentials.get("snmp_v3") + if snmp_v3: - snmpV3Id = snmp_v3.get("id") - snmpV3Description = snmp_v3.get("description") - if snmpV3Id or snmpV3Description: + snmp_v3_id = snmp_v3.get("id") + snmp_v3_description = snmp_v3.get("description") + if snmp_v3_id or snmp_v3_description: # All snmp_v3 details from the Cisco Catalyst Center - snmpV3_details = global_credentials.get("snmpV3") - if not snmpV3_details: + global_snmp_v3_details = global_credentials.get("snmpV3") + + if not global_snmp_v3_details: self.msg = "Global snmp_v3 Credential is not available." self.status = "failed" return self - snmpV3Detail = None - if snmpV3Id: - snmpV3Detail = get_dict_result( - snmpV3_details, "id", snmpV3Id) - if not snmpV3Detail: + snmp_v3_detail = None + + if snmp_v3_id: + snmp_v3_detail = get_dict_result( + global_snmp_v3_details, "id", snmp_v3_id) + if not snmp_v3_detail: self.msg = "The ID of the snmp_v3 credential is not valid." self.status = "failed" return self - elif snmpV3Description: - for item in snmpV3_details: - if item.get("description") == snmpV3Description: - snmpV3Detail = item - if not snmpV3Detail: + elif snmp_v3_description: + for item in global_snmp_v3_details: + if item.get("description") == snmp_v3_description: + snmp_v3_detail = item + if not snmp_v3_detail: self.msg = "The username and description for the snmp_v2c_write credential are invalid." self.status = "failed" return self - want["apply_credentials"]["snmpV3Id"] = snmpV3Detail.get("id") + want["apply_credentials"]["snmpV3Id"] = snmp_v3_detail.get("id") self.log("Desired State (want): {0}".format(want), "INFO") self.want.update(want) @@ -2483,16 +2526,16 @@ def get_want(self, config): """ if config.get("global_credential_details"): - CredentialDetails = config.get("global_credential_details") - self.get_want_device_credentials(CredentialDetails).check_return_status() + credential_details = config.get("global_credential_details") + self.get_want_device_credentials(credential_details).check_return_status() if config.get("assign_credentials_to_site"): - AssignCredentials = config.get("assign_credentials_to_site") - self.get_want_assign_credentials(AssignCredentials).check_return_status() + assign_credentials = config.get("assign_credentials_to_site") + self.get_want_assign_credentials(assign_credentials).check_return_status() if config.get("apply_credentials_to_site"): - ApplyCredentials = config.get("apply_credentials_to_site") - self.get_want_apply_credentials(ApplyCredentials).check_return_status() + apply_credentials = config.get("apply_credentials_to_site") + self.get_want_apply_credentials(apply_credentials).check_return_status() self.log("Desired State (want): {0}".format(self.want), "INFO") self.msg = "Successfully retrieved details from the playbook" @@ -2512,7 +2555,7 @@ def create_device_credentials(self): self """ - result_global_credential = self.result.get("response")[0].get("globalCredential") + result_global_credential = self.result.get("response")[0].get("global_credential") want_create = self.want.get("want_create") if not want_create: result_global_credential.update({ @@ -2534,6 +2577,7 @@ def create_device_credentials(self): ) self.log("Received API response from 'create_global_credentials_v2': {0}" .format(response), "DEBUG") + if self.get_ccc_version_as_integer() <= self.get_ccc_version_as_int_from_str("2.3.5.3"): validation_string = "global credential addition performed" self.check_task_response_status(response, validation_string, "create_global_credentials_v2").check_return_status() @@ -2563,7 +2607,7 @@ def update_device_credentials(self): self """ - result_global_credential = self.result.get("response")[0].get("globalCredential") + result_global_credential = self.result.get("response")[0].get("global_credential") # Get the result global credential and want_update from the current object want_update = self.want.get("want_update") @@ -2603,11 +2647,13 @@ def update_device_credentials(self): ) self.log("Received API response for 'update_global_credentials_v2': {0}" .format(response), "DEBUG") + if self.get_ccc_version_as_integer() <= self.get_ccc_version_as_int_from_str("2.3.5.3"): validation_string = "global credential update performed" self.check_task_response_status(response, validation_string, "update_global_credentials_v2").check_return_status() else: self.check_tasks_response_status(response, "update_global_credentials_v2").check_return_status() + self.log("Updating device credential API input parameters: {0}" .format(final_response), "DEBUG") self.log("Global device credential updated successfully", "INFO") @@ -2634,11 +2680,12 @@ def assign_credentials_to_site(self): self """ - result_assign_credential = self.result.get("response")[0].get("assignCredential") + result_assign_credential = self.result.get("response")[0].get("assign_credential") credential_params = self.want.get("assign_credentials") final_response = [] self.log("Assigning device credential to site API input parameters: {0}" .format(credential_params), "DEBUG") + if not credential_params: result_assign_credential.update({ "No Assign Credentials": { @@ -2651,6 +2698,7 @@ def assign_credentials_to_site(self): return self site_ids = self.want.get("site_id") + for site_id in site_ids: if self.get_ccc_version_as_integer() <= self.get_ccc_version_as_int_from_str("2.3.5.3"): credential_params.update({"site_id": site_id}) @@ -2765,12 +2813,14 @@ def apply_credentials_to_site(self): """ site_ids = self.want.get("site_id") site_names = self.want.get("site_name") + for site_id, site_name in zip(site_ids, site_names): if self.get_ccc_version_as_integer() >= self.get_ccc_version_as_int_from_str("2.3.7.6"): - result_apply_credential = self.result.get("response")[0].get("applyCredential") + result_apply_credential = self.result.get("response")[0].get("apply_credential") credential_params = self.want.get("apply_credentials") final_response = [] self.log("Applying device credential to site API input parameters: {0}".format(credential_params), "DEBUG") + if not credential_params: result_apply_credential.update({ "No Apply Credentials": { @@ -2782,8 +2832,9 @@ def apply_credentials_to_site(self): self.status = "success" return self - site_response = self.get_device_ids_from_site(site_id) - if not site_response: + site_response = self.get_device_ids_from_site(site_name, site_id) + + if not site_response[1]: result_apply_credential.update({ "No Apply Credentials": { "response": "No Response", @@ -2825,8 +2876,8 @@ def apply_credentials_to_site(self): valid_sync_cred_ids.append(id) else: invalid_sync_cred_ids.append(id) - self.log("Credential IDs {0} not assigned to site, so Sync not possible.".format(invalid_sync_cred_ids), "INFO") + if not valid_sync_cred_ids: result_apply_credential.update({ "Applied Credentials": { @@ -2917,8 +2968,8 @@ def delete_device_credential(self, config): self """ - result_global_credential = self.result.get("response")[0].get("globalCredential") - have_values = self.have.get("globalCredential") + result_global_credential = self.result.get("response")[0].get("global_credential") + have_values = self.have.get("global_credential") final_response = {} self.log("Global device credentials to be deleted: {0}".format(have_values), "DEBUG") credential_mapping = { @@ -2938,6 +2989,7 @@ def delete_device_credential(self, config): config_itr = config_itr + 1 description = config.get("global_credential_details") \ .get(credential_mapping.get(item))[config_itr].get("description") + if value is None: self.log("Credential Name: {0}".format(item), "DEBUG") self.log("Credential Item: {0}".format(config.get("global_credential_details") @@ -2960,6 +3012,7 @@ def delete_device_credential(self, config): .format(response), "DEBUG") validation_string = "global credential deleted successfully" response = response.get("response") + if response.get("errorcode") is not None: self.msg = response.get("response").get("detail") self.status = "failed" @@ -3003,6 +3056,7 @@ def delete_device_credential(self, config): "response": final_response, } }) + if failed_status is True: self.msg = "Global device credentials are not deleted." self.module.fail_json(msg=self.msg, response=final_response) @@ -3067,6 +3121,7 @@ def verify_diff_merged(self, config): "httpsWrite": ["description", "username", "port", "id"], "snmpV3": ["username", "description", "snmpMode", "id"] } + for credential_type in credential_types: if self.want.get(credential_type): want_credential = self.want.get(credential_type) @@ -3081,15 +3136,15 @@ def verify_diff_merged(self, config): return self self.log("Successfully validated global device credential", "INFO") - self.result.get("response")[0].get("globalCredential").update({"Validation": "Success"}) + self.result.get("response")[0].get("global_credential").update({"Validation": "Success"}) if config.get("assign_credentials_to_site") is not None: self.log("Successfully validated the assign device credential to site", "INFO") - self.result.get("response")[0].get("assignCredential").update({"Validation": "Success"}) + self.result.get("response")[0].get("assign_credential").update({"Validation": "Success"}) if config.get("apply_credentials_to_site") is not None: self.log("Successfully validated the assign device credential to site", "INFO") - self.result.get("response")[0].get("applyCredential").update({"Validation": "Success"}) + self.result.get("response")[0].get("apply_credential").update({"Validation": "Success"}) self.msg = "Successfully validated the global device credential, assigned and applied device credential to site." self.status = "success" @@ -3113,7 +3168,7 @@ def verify_diff_deleted(self, config): self.log("Desired State (want): {0}".format(self.want), "INFO") if config.get("global_credential_details") is not None: - have_global_credential = self.have.get("globalCredential") + have_global_credential = self.have.get("global_credential") credential_types = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", "httpsRead", "httpsWrite", "snmpV3"] for credential_type in credential_types: @@ -3127,7 +3182,7 @@ def verify_diff_deleted(self, config): return self self.log("Successfully validated absence of global device credential.", "INFO") - self.result.get("response")[0].get("globalCredential").update({"Validation": "Success"}) + self.result.get("response")[0].get("global_credential").update({"Validation": "Success"}) self.msg = "Successfully validated the absence of Global Device Credential." self.status = "success" diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 25c5c7a012..5bb3345dce 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -89,17 +89,17 @@ or resyncing devices, with Meraki devices being the exception. elements: str type: list - hostname_list: + hostnames: description: "A list of hostnames representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses." type: list elements: str - serial_number_list: + serial_numbers: description: A list of serial numbers representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses. type: list elements: str - mac_address_list: + mac_addresses: description: "A list of MAC addresses representing devices. Operations such as updating, deleting, resyncing, or rebooting can be performed as alternatives to using IP addresses." type: list @@ -771,9 +771,9 @@ def validate_input(self): 'http_secure': {'type': 'bool'}, 'http_username': {'type': 'str'}, 'ip_address_list': {'type': 'list', 'elements': 'str'}, - 'hostname_list': {'type': 'list', 'elements': 'str'}, - 'serial_number_list': {'type': 'list', 'elements': 'str'}, - 'mac_address_list': {'type': 'list', 'elements': 'str'}, + 'hostnames': {'type': 'list', 'elements': 'str'}, + 'serial_numbers': {'type': 'list', 'elements': 'str'}, + 'mac_addresses': {'type': 'list', 'elements': 'str'}, 'netconf_port': {'type': 'str'}, 'password': {'type': 'str'}, 'snmp_auth_passphrase': {'type': 'str'}, @@ -864,25 +864,28 @@ def get_device_ips_from_config_priority(self): If none of the information is available, an empty list is returned. """ # Retrieve device IPs from the configuration - device_ips = self.config[0].get("ip_address_list") + device_ips = self.want.get("device_params").get("ipAddress") if device_ips: return device_ips # If device IPs are not available, check hostnames - device_hostnames = self.config[0].get("hostname_list") + device_hostnames = self.config[0].get("hostnames") if device_hostnames: - return self.get_device_ips_from_hostname(device_hostnames) + device_ip_dict = self.get_device_ips_from_hostnames(device_hostnames) + return self.get_list_from_dict_values(device_ip_dict) # If hostnames are not available, check serial numbers - device_serial_numbers = self.config[0].get("serial_number_list") + device_serial_numbers = self.config[0].get("serial_numbers") if device_serial_numbers: - return self.get_device_ips_from_serial_number(device_serial_numbers) + device_ip_dict = self.get_device_ips_from_serial_numbers(device_serial_numbers) + return self.get_list_from_dict_values(device_ip_dict) # If serial numbers are not available, check MAC addresses - device_mac_addresses = self.config[0].get("mac_address_list") + device_mac_addresses = self.config[0].get("mac_addresses") if device_mac_addresses: - return self.get_device_ips_from_mac_address(device_mac_addresses) + device_ip_dict = self.get_device_ips_from_mac_addresses(device_mac_addresses) + return self.get_list_from_dict_values(device_ip_dict) # If no information is available, return an empty list return [] @@ -1465,7 +1468,7 @@ def reboot_access_points(self): return self # Get and store the apEthernetMacAddress of given devices - ap_mac_address_list = [] + ap_mac_addresses = [] for device_ip in input_device_ips: response = self.dnac._exec( family="devices", @@ -1481,9 +1484,9 @@ def reboot_access_points(self): ap_mac_address = response.get('apEthernetMacAddress') if ap_mac_address is not None: - ap_mac_address_list.append(ap_mac_address) + ap_mac_addresses.append(ap_mac_address) - if not ap_mac_address_list: + if not ap_mac_addresses: self.status = "success" self.result['changed'] = False self.msg = "Cannot find the AP devices for rebooting" @@ -1493,7 +1496,7 @@ def reboot_access_points(self): # Now call the Reboot Access Point API reboot_params = { - "apMacAddresses": ap_mac_address_list + "apMacAddresses": ap_mac_addresses } response = self.dnac._exec( family="wireless", @@ -2239,12 +2242,12 @@ def get_device_ids(self, device_ips): return device_ids - def get_device_ips_from_hostname(self, hostname_list): + def get_device_ips_from_hostnames(self, hostnames): """ Get the list of unique device IPs for list of specified hostnames of devices in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - hostname_list (list): The hostnames of devices for which you want to retrieve the device IPs. + hostnames (list): The hostnames of devices for which you want to retrieve the device IPs. Returns: list: The list of unique device IPs for the specified devices hostname list. Description: @@ -2253,7 +2256,7 @@ def get_device_ips_from_hostname(self, hostname_list): """ device_ips = [] - for hostname in hostname_list: + for hostname in hostnames: try: response = self.dnac._exec( family="devices", @@ -2274,12 +2277,12 @@ def get_device_ips_from_hostname(self, hostname_list): return device_ips - def get_device_ips_from_serial_number(self, serial_number_list): + def get_device_ips_from_serial_numbers(self, serial_numbers): """ Get the list of unique device IPs for a specified list of serial numbers in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - serial_number_list (list): The list of serial number of devices for which you want to retrieve the device IPs. + serial_numbers (list): The list of serial number of devices for which you want to retrieve the device IPs. Returns: list: The list of unique device IPs for the specified devices with serial numbers. Description: @@ -2288,7 +2291,7 @@ def get_device_ips_from_serial_number(self, serial_number_list): """ device_ips = [] - for serial_number in serial_number_list: + for serial_number in serial_numbers: try: response = self.dnac._exec( family="devices", @@ -2309,12 +2312,12 @@ def get_device_ips_from_serial_number(self, serial_number_list): return device_ips - def get_device_ips_from_mac_address(self, mac_address_list): + def get_device_ips_from_mac_addresses(self, mac_addresses): """ Get the list of unique device IPs for list of specified mac address of devices in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - mac_address_list (list): The list of mac address of devices for which you want to retrieve the device IPs. + mac_addresses (list): The list of mac address of devices for which you want to retrieve the device IPs. Returns: list: The list of unique device IPs for the specified devices. Description: @@ -2323,7 +2326,7 @@ def get_device_ips_from_mac_address(self, mac_address_list): """ device_ips = [] - for mac_address in mac_address_list: + for mac_address in mac_addresses: try: response = self.dnac._exec( family="devices", diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index ebb281fb71..2e52408b53 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -878,17 +878,20 @@ def get_device_ips_from_config_priority(self): # If device IPs are not available, check hostnames device_hostnames = self.config[0].get("hostname_list") if device_hostnames: - return self.get_device_ips_from_hostname(device_hostnames) + device_ip_dict = self.get_device_ips_from_hostnames(device_hostnames) + return self.get_list_from_dict_values(device_ip_dict) # If hostnames are not available, check serial numbers device_serial_numbers = self.config[0].get("serial_number_list") if device_serial_numbers: - return self.get_device_ips_from_serial_number(device_serial_numbers) + device_ip_dict = self.get_device_ips_from_serial_numbers(device_serial_numbers) + return self.get_list_from_dict_values(device_ip_dict) # If serial numbers are not available, check MAC addresses device_mac_addresses = self.config[0].get("mac_address_list") if device_mac_addresses: - return self.get_device_ips_from_mac_address(device_mac_addresses) + device_ip_dict = self.get_device_ips_from_mac_addresses(device_mac_addresses) + return self.get_list_from_dict_values(device_ip_dict) # If no information is available, return an empty list return [] @@ -1830,7 +1833,7 @@ def provision_wired_device_v2(self, device_ip, site_name): assign_params = {'deviceIds': [device_id], 'siteId': site_id} provision_params = [{"siteId": site_id, "networkDeviceId": device_id}] - is_device_provisioned = self.is_device_provisioned(device_id) + is_device_provisioned = self.is_device_provisioned(device_id, device_ip) is_device_assigned_to_site = self.is_device_assigned_to_site(device_id) if not is_device_assigned_to_site: @@ -2398,114 +2401,6 @@ def get_device_ids(self, device_ips): return device_ids - def get_device_ips_from_hostname(self, hostname_list): - """ - Get the list of unique device IPs for list of specified hostnames of devices in Cisco Catalyst Center. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - hostname_list (list): The hostnames of devices for which you want to retrieve the device IPs. - Returns: - list: The list of unique device IPs for the specified devices hostname list. - Description: - Queries Cisco Catalyst Center to retrieve the unique device IP's associated with a device having the specified - list of hostnames. If a device is not found in Cisco Catalyst Center, an error log message is printed. - """ - - device_ips = [] - for hostname in hostname_list: - try: - response = self.dnac._exec( - family="devices", - function='get_device_list', - op_modifies=True, - params={"hostname": hostname} - ) - if response: - self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") - response = response.get("response") - if response: - device_ip = response[0]["managementIpAddress"] - if device_ip: - device_ips.append(device_ip) - except Exception as e: - error_message = "Exception occurred while fetching device from Cisco Catalyst Center: {0}".format(str(e)) - self.log(error_message, "ERROR") - - return device_ips - - def get_device_ips_from_serial_number(self, serial_number_list): - """ - Get the list of unique device IPs for a specified list of serial numbers in Cisco Catalyst Center. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - serial_number_list (list): The list of serial number of devices for which you want to retrieve the device IPs. - Returns: - list: The list of unique device IPs for the specified devices with serial numbers. - Description: - Queries Cisco Catalyst Center to retrieve the unique device IPs associated with a device having the specified - serial numbers.If a device is not found in Cisco Catalyst Center, an error log message is printed. - """ - - device_ips = [] - for serial_number in serial_number_list: - try: - response = self.dnac._exec( - family="devices", - function='get_device_list', - op_modifies=True, - params={"serialNumber": serial_number} - ) - if response: - self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") - response = response.get("response") - if response: - device_ip = response[0]["managementIpAddress"] - if device_ip: - device_ips.append(device_ip) - except Exception as e: - error_message = "Exception occurred while fetching device from Cisco Catalyst Center - {0}".format(str(e)) - self.log(error_message, "ERROR") - - return device_ips - - def get_device_ips_from_mac_address(self, mac_address_list): - """ - Get the list of unique device IPs for list of specified mac address of devices in Cisco Catalyst Center. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - mac_address_list (list): The list of mac address of devices for which you want to retrieve the device IPs. - Returns: - list: The list of unique device IPs for the specified devices. - Description: - Queries Cisco Catalyst Center to retrieve the unique device IPs associated with a device having the specified - mac addresses. If a device is not found in Cisco Catalyst Center, an error log message is printed. - """ - - device_ips = [] - for mac_address in mac_address_list: - try: - response = self.dnac._exec( - family="devices", - function='get_device_list', - op_modifies=True, - params={"macAddress": mac_address} - ) - if response: - self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") - response = response.get("response") - if response: - device_ip = response[0]["managementIpAddress"] - if device_ip: - device_ips.append(device_ip) - except Exception as e: - self.status = "failed" - self.msg = "Exception occurred while fetching device from Cisco Catalyst Center - {0}".format(str(e)) - self.result['response'] = self.msg - self.log(self.msg, "ERROR") - self.check_return_status() - - return device_ips - def get_interface_from_id_and_name(self, device_id, interface_name): """ Retrieve the interface ID for a device in Cisco Catalyst Center based on device id and interface name. @@ -3869,11 +3764,12 @@ def delete_provisioned_device_v1(self, device_ip): op_modifies=True, params=provision_params, ) - self.log("Received API response from 'delete_provisioned_wired_device': {0}".format(str(response)), "DEBUG") - - validation_string = "deleted successfully" - self.check_task_response_status(response, validation_string, 'delete_provisioned_wired_device') - self.deleted_devices.append(device_ip) + if response: + response = {"response": response} + self.log("Received API response from 'delete_provisioned_wired_device': {0}".format(str(response)), "DEBUG") + validation_string = "deleted successfully" + self.check_task_response_status(response, validation_string, 'delete_provisioned_wired_device') + self.provisioned_device_deleted.append(device_ip) def delete_provisioned_device_v2(self, device_ip): """ diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 923b5ba549..509a83f5e8 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -559,7 +559,9 @@ def get_obj_params(self, get_object): obj_params = [ ("protocol", "protocol"), ("retries", "retries"), - ("timeoutSeconds", "timeoutSeconds") + ("timeoutSeconds", "timeoutSeconds"), + ("pxgridEnabled", "pxgridEnabled"), + ("useDnacCertForPxgrid", "useDnacCertForPxgrid"), ] else: raise ValueError("Received an unexpected value for 'get_object': {0}" @@ -1368,6 +1370,51 @@ def check_auth_server_response_status(self, response, validation_string_set, api return self + def check_ise_server_updation_status(self, have_auth_details, want_auth_details): + """ + Check if the Cisco ISE server requires an update by comparing the user name, + FQDN, and the state of the Cisco ISE server. + + Parameters: + have_auth_details (dict) - Current Cisco Catalyst Center authentication server information. + want_auth_details (dict) - Desired authentication server configuration from the playbook. + Returns: + True or False (bool): True if the Cisco ISE server requires an update; otherwise, False. + Description: + Compares the user name and FQDN between the existing configuration and the new configuration. + If there is a discrepancy, or if the server state is not 'ACTIVE', an update is required. + """ + + ip_address = have_auth_details.get("ipAddress") + have_cisco_ise_dtos = have_auth_details.get("ciscoIseDtos")[0] + want_cisco_ise_dtos = want_auth_details.get("ciscoIseDtos")[0] + self.log( + "Checking if the Cisco ISE server '{ip_address}' requires an update." + .format(ip_address=ip_address), "DEBUG" + ) + check_list = ["userName", "fqdn"] + for item in check_list: + if have_cisco_ise_dtos[item] != want_cisco_ise_dtos[item]: + self.log( + "Cisco ISE server '{ip_address}' requires an update: {item} has changed." + .format(item=item, ip_address=ip_address), "INFO" + ) + return True + + state = have_auth_details.get("state") + if state != "ACTIVE": + self.log( + "Cisco ISE server '{ip_address}' is not in 'ACTIVE' state (current state: '{state}'). " + "Update required.".format(ip_address=ip_address, state=state), "DEBUG" + ) + return True + + self.log( + "No updates are required for the Cisco ISE server '{ip_address}' based on username, fqdn, and state." + .format(ip_address=ip_address), "DEBUG" + ) + return False + def format_payload_for_update(self, have_auth_server, want_auth_server): """ Format the parameter of the payload for updating the authentication and policy server @@ -1500,17 +1547,33 @@ def update_auth_policy_server(self, authentication_policy_server): # Authentication and Policy Server exists, check update is required # Edit API not working, remove this - self.format_payload_for_update(self.have.get("authenticationPolicyServer")[auth_server_index].get("details"), - self.want.get("authenticationPolicyServer")[auth_server_index]).check_return_status() - is_ise_server_enabled = self.have.get("authenticationPolicyServer")[auth_server_index].get("details").get("isIseEnabled") - if not (is_ise_server_enabled or self.requires_update(self.have.get("authenticationPolicyServer")[auth_server_index].get("details"), - self.want.get("authenticationPolicyServer")[auth_server_index], - self.authentication_policy_server_obj_params)): + have_auth_server_details = self.have.get("authenticationPolicyServer")[auth_server_index].get("details") + want_auth_server_details = self.want.get("authenticationPolicyServer")[auth_server_index] + + self.log( + "Formatting payload for update between current and desired authentication server details.", "DEBUG" + ) + self.format_payload_for_update(have_auth_server_details, want_auth_server_details).check_return_status() + + is_ise_server_enabled = have_auth_server_details.get("isIseEnabled") + ise_server_requires_update = False + if is_ise_server_enabled: + self.log("Cisco ISE server is enabled; checking if an update is required.") + ise_server_requires_update = self.check_ise_server_updation_status(have_auth_server_details, + want_auth_server_details) + if ise_server_requires_update: + self.log("Cisco ISE server requires an update based on configuration changes.", "DEBUG") + else: + self.log("Cisco ISE server does not require any updates.", "DEBUG") + + if not (ise_server_requires_update or self.requires_update(have_auth_server_details, + want_auth_server_details, + self.authentication_policy_server_obj_params)): self.log("Authentication and Policy Server '{0}' doesn't require an update" .format(ip_address), "INFO") result_auth_server.get("response").get(ip_address).update({ "Cisco Catalyst Center params": - self.have.get("authenticationPolicyServer")[auth_server_index].get("details") + have_auth_server_details }) result_auth_server.get("response").get(ip_address).update({ "Id": self.have.get("authenticationPolicyServer")[auth_server_index].get("id") @@ -1523,12 +1586,12 @@ def update_auth_policy_server(self, authentication_policy_server): self.log("Authentication and Policy Server requires update", "DEBUG") # Authenticaiton and Policy Server Exists - auth_server_params = copy.deepcopy(self.want.get("authenticationPolicyServer")[auth_server_index]) + auth_server_params = copy.deepcopy(want_auth_server_details) auth_server_params.update({"id": self.have.get("authenticationPolicyServer")[auth_server_index].get("id")}) self.log("Desired State for Authentication and Policy Server (want): {0}" .format(auth_server_params), "DEBUG") self.log("Current State for Authentication and Policy Server (have): {0}" - .format(self.have.get("authenticationPolicyServer")[auth_server_index].get("details")), "DEBUG") + .format(have_auth_server_details), "DEBUG") function_name = "edit_authentication_and_policy_server_access_configuration" response = self.dnac._exec( family="system_settings", @@ -1548,7 +1611,7 @@ def update_auth_policy_server(self, authentication_policy_server): trusted_server_msg = "" if is_ise_server_enabled: trusted_server = self.want.get("trusted_server") - state = self.have.get("authenticationPolicyServer")[auth_server_index].get("details").get("state") + state = have_auth_server_details.get("state") if state != "ACTIVE": self.check_ise_server_integration_status(ip_address) self.accept_cisco_ise_server_certificate(ip_address, trusted_server) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 7f0fc66962..dc1851a65d 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,119 @@ 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 = "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) + + # 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): + """ + 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.fail_and_exit(self.msg) + + self.log("Validation successful: Network Compliance operation present") + + def validate_run_compliance_categories(self, run_compliance_categories): + """ + 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): + 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): + """ + 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) + + # 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 + 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 +574,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 +590,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 +613,182 @@ 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): + """ + 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()) + } + + 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) + + 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] + 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("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): + """ + 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. """ - 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. + 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("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): + """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. """ + 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 - 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} + op_modifies=False, + 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") + 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 reachability_status == "Reachable" and collection_status == "Managed": + # Skip Unified AP devices + if device_family != "Unified AP" : + 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_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, reachability_status, collection_status + ) + ) + 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 +796,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: @@ -589,21 +805,34 @@ 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: + 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 = self.get_device_ip_from_device_id(site_id) - 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 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 +885,39 @@ 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: + self.log( + "No Compliance details retrieved for device: '{0}' with parameters: {1}".format( + device_ip, compliance_details_of_device_params + ), + "WARNING" + ) + return None 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 +960,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 +1012,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 +1033,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 +1063,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 +1074,36 @@ 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) - self.log("The task status of batch: {0} with task id: {1} is {2}".format(idx, task_id, task_status), "INFO") + 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) - # Extract message and status from the task status result - msg = task_status[task_id]["msg"] - status = task_status[task_id]["status"] + 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") # 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 +1115,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 +1137,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 +1167,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 +1195,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 +1245,230 @@ 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() + 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 = {} - while True: - success_devices = [] - failed_devices = [] + # 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") - response = self.check_task_tree_response(task_id, task_name) + # 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") - # 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 + # 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} + + # 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 + 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 ( + 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") + 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") + 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) + 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 " + self.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 +1476,70 @@ 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: + 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 + } + + 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 + } + + 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 +1549,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 +1616,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/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 5bafb99fbd..20390e7cd5 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -820,6 +820,26 @@ def validate_input(self): } } + invalid_params_type = [] + + for config_item in self.config: + ip_pool = config_item.get("global_pool_details", {}).get("settings", {}).get("ip_pool", []) + + for pool in ip_pool: + # Check for 'dhcp_server_ips' + if not isinstance(pool["dhcp_server_ips"], list): + invalid_params_type.append("'dhcp_server_ips' should be a list.") + + # Check for 'dns_server_ips' + if not isinstance(pool["dns_server_ips"], list): + invalid_params_type.append("'dns_server_ips' should be a list.") + + if invalid_params_type: + self.msg = "Invalid required parameter(s): {0}".format(', '.join(invalid_params_type)) + self.result['response'] = self.msg + self.status = "failed" + return self + # Validate playbook params against the specification (temp_spec) valid_temp, invalid_params = validate_list_of_dicts(self.config, temp_spec) if invalid_params: @@ -2301,7 +2321,7 @@ def get_want_reserve_pool(self, reserve_pool): self.status = "failed" return self - if not pool_values.get("ipv4PrefixLength"): + if pool_values.get("ipv4Prefix") and not pool_values.get("ipv4PrefixLength"): self.msg = "missing parameter 'ipv4_prefix_length' in reserve_pool_details '{0}' element" \ .format(reserve_pool_index + 1) self.status = "failed" @@ -2320,7 +2340,7 @@ def get_want_reserve_pool(self, reserve_pool): if pool_values.get("ipv4TotalHost") is None: del pool_values['ipv4TotalHost'] if pool_values.get("ipv6AddressSpace") is True: - pool_values.update({"ipv6Prefix": True}) + pool_values.update({"ipv6AddressSpace": True}) else: del pool_values['ipv6Prefix'] diff --git a/plugins/modules/pnp_workflow_manager.py b/plugins/modules/pnp_workflow_manager.py index 57e4b27341..aff7ee65e1 100644 --- a/plugins/modules/pnp_workflow_manager.py +++ b/plugins/modules/pnp_workflow_manager.py @@ -690,9 +690,9 @@ def get_have(self): # check if given device exists in pnp inventory, store device Id device_response = self.get_device_list_pnp(self.want.get("serial_number")) self.log("Device details for the device with serial \ - number '{0}': {1}".format(self.want.get("serial_number"), str(device_response)), "DEBUG") + number '{0}': {1}".format(self.want.get("serial_number"), self.pprint(device_response)), "DEBUG") - if not (device_response and (len(device_response) == 1)): + if not device_response: self.log("Device with serial number {0} is not found in the inventory".format(self.want.get("serial_number")), "WARNING") self.msg = "Adding the device to database" self.status = "success" @@ -701,7 +701,7 @@ def get_have(self): return self have["device_found"] = True - have["device_id"] = device_response[0].get("id") + have["device_id"] = device_response.get("id") self.log("Device Id: " + str(have["device_id"])) if self.params.get("state") == "merged": @@ -710,27 +710,27 @@ def get_have(self): family="software_image_management_swim", function='get_software_image_details', params=self.want.get("image_params"), - op_modifies=True, ) image_list = image_response.get("response") - self.log("Image details obtained from the API 'get_software_image_details': {0}".format(str(image_response)), "DEBUG") + self.log("Image details obtained from the API 'get_software_image_details': {0}" + .format(self.pprint(image_response)), "DEBUG") # check if project has templates or not template_list = self.dnac_apply['exec']( family="configuration_templates", function='gets_the_templates_available', params={"project_names": self.want.get("project_name")}, - op_modifies=True, ) - self.log("List of templates under the project '{0}': {1}".format(self.want.get("project_name"), - str(template_list)), "DEBUG") + self.log("List of templates under the project '{0}': {1}" + .format(self.want.get("project_name"), self.pprint(template_list)), "DEBUG") - dev_details_response = self.get_device_by_id_pnp(device_response[0].get("id")) - self.log("Device details retrieved after calling the 'get_device_by_id' API: {0}".format(str(dev_details_response)), "DEBUG") + dev_details_response = self.get_device_by_id_pnp(device_response.get("id")) + self.log("Device details retrieved after calling the 'get_device_by_id' API: {0}" + .format(self.pprint(dev_details_response)), "DEBUG") install_mode = dev_details_response.get("deviceInfo").get("mode") - self.log("Installation mode of the device with the serial no. '{0}':{1}".format(self.want.get("serial_number"), - install_mode), "INFO") + self.log("Installation mode of the device with the serial no. '{0}':{1}" + .format(self.want.get("serial_number"), install_mode), "INFO") # check if given site exits, if exists store current site info site_exists = False @@ -1087,8 +1087,8 @@ def get_diff_deleted(self): multi_device_response = self.get_device_list_pnp(device["deviceInfo"]["serialNumber"]) self.log("Response from 'get_device_list' API for claiming: {0}".format(str(multi_device_response)), "DEBUG") - if multi_device_response and len(multi_device_response) == 1: - device_id = multi_device_response[0].get("id") + if multi_device_response: + device_id = multi_device_response.get("id") response = self.dnac_apply['exec']( family="device_onboarding_pnp", @@ -1217,7 +1217,7 @@ def get_device_list_pnp(self, serial_number): msg = "No device found with serial number: {0}".format(serial_number) self.log(msg, "WARNING") - self.module.fail_json(msg=msg) + return None except Exception as e: msg = "An error occurred while retrieving device with serial number {0}: {1}".format(serial_number, str(e)) @@ -1317,7 +1317,6 @@ def pnp_device_count(self, pnp_params): prov_dev_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function='get_device_count', - op_modifies=True, params=pnp_params, ) if prov_dev_response: diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index fd7fd75eb7..c5bedf2eec 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -73,6 +73,20 @@ - This is mandatory for provisioning of wireless devices. type: list elements: str + primary_managed_ap_locations: + description: + - List of site locations assigned to primary managed Access Points (APs). + - Required for provisioning wireless devices if the managed AP location is not set. + - Supported in Cisco Catalyst version 2.3.7.6 and later. + type: list + elements: str + secondary_managed_ap_locations: + description: + - List of site locations assigned to secondary managed Access Points (APs). + - Required for provisioning wireless devices if the managed AP location is not set. + - Supported in Cisco Catalyst version 2.3.7.6 and later. + type: list + elements: str dynamic_interfaces: description: - A list of dynamic interfaces on the wireless controller. @@ -81,10 +95,10 @@ elements: dict suboptions: interface_name: - description: The name of the interface + description: The name of the interface. (Required parameter for Cisco Catalyst Version - 2.3.7.6) type: str vlan_id: - description: The VLAN ID associated with the interface. + description: The VLAN ID associated with the interface. (Required parameter for Cisco Catalyst Version - 2.3.7.6) type: str interface_ip_address: description: The IP address assigned to the interface. @@ -102,26 +116,29 @@ description: - If set to 'true', Access Point (AP) provisioning will be skipped during the workflow. - Use this option when AP provisioning is not required as part of the current operation. + - Supported in Cisco Catalyst version 2.3.7.6 and later. type: bool default: false rolling_ap_upgrade: description: - Configuration options for performing a rolling upgrade of Access Points (APs) in phases. - Allows control over the gradual rebooting of APs during the upgrade process. + - Supported in Cisco Catalyst version 2.3.7.6 and later. type: dict suboptions: enable_rolling_ap_upgrade: description: - Enable or disable the rolling AP upgrade feature. - If set to 'true', APs will be upgraded in batches based on the specified reboot percentage. + - Supported in Cisco Catalyst version 2.3.7.6 and later. type: bool default: false ap_reboot_percentage: description: - The percentage of APs to reboot simultaneously during an upgrade. - - Must be an integer value, typically between 1 and 100, indicating the proportion of APs to upgrade at a time. + - Supported in Cisco Catalyst version 2.3.7.6 and later. + - Must be either 5, 15 or 25 representing the proportion of APs to reboot at once. type: int - default: 20 requirements: - dnacentersdk == 2.4.5 @@ -190,7 +207,7 @@ primary_managed_ap_Locations: - Global/USA/San Francisco/BGL_18/Test_Floor2 secondary_managed_ap_locations: - -Global/USA/San Francisco/BGL_18/Test_Floor1 + - Global/USA/San Francisco/BGL_18/Test_Floor1 dynamic_interfaces: - interface_name: Vlan1866 vlan_id: 1866 @@ -678,6 +695,42 @@ def is_device_assigned_to_site(self, uuid): self.log(msg, "CRITICAL") self.module.fail_json(msg=msg) + def get_device_site_by_uuid(self, uuid): + """ + Checks if a device is assigned to any site. + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + - uuid (str): The UUID of the device to check for site assignment. + Returns: + - location (str): The location of the site if the device is assigned, + None otherwise. + """ + + self.log("Checking site assignment for device with UUID: {0}".format(uuid), "INFO") + + try: + site_response = self.dnac_apply['exec']( + family="devices", + function='get_device_detail', + params={"search_by": uuid, "identifier": "uuid"} + ) + self.log("Response collected from the API 'get_device_detail': {0}".format(site_response)) + + site_response = site_response.get("response") + if site_response and site_response.get("location"): + location = site_response.get("location") + return location + else: + self.log("No site assignment found for device with UUID: {0}".format(uuid), "INFO") + return None + + except Exception as e: + msg = "Failed to find device with location for UUID {0} due to: {1}".format(uuid, e) + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + def get_wired_params(self): """ Prepares the payload for provisioning of the wired devices @@ -774,6 +827,15 @@ def get_wireless_params(self): has_interface_name = False has_vlan_id = False + if interfaces is None: + self.msg = ("It appears that the 'dynamic_interfaces' parameter is either missing or set to None. " + "Please note that this parameter is required for provisioning a wireless device in " + "Catalyst Center version 2.3.7.6 or higher.") + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + self.status = "failed" + self.check_return_status() + for interface in interfaces: if 'interface_name' in interface: has_interface_name = True @@ -790,8 +852,8 @@ def get_wireless_params(self): if missing_fields: missing_fields_str = ', '.join(missing_fields) - self.msg = ("The following fields are mandatory to provision a wireless device in 2.3.7.6 and" - "are missing: {0}".format(missing_fields_str), "CRITICAL") + self.msg = ("The following required fields for provisioning a wireless device in version" + " 2.3.7.6 are currently missing: {0}".format(missing_fields_str)) self.log(self.msg, "ERROR") self.result['response'] = self.msg self.status = "failed" @@ -878,6 +940,18 @@ def perform_wireless_reprovision(self): If wireless device is already provisioned, this method calls the provision update API and handles it accordingly """ + device_id = self.get_device_id() + self.log("Retrieved device ID: {0}".format(device_id), "DEBUG") + already_provisioned_site = self.get_device_site_by_uuid(device_id) + + if already_provisioned_site != self.site_name: + self.log("Device re-provisioning logic triggered.", "INFO") + self.msg = ("Error in re-provisioning a wireless device '{0}' - the device is already associated " + "with Site: {1} and cannot be re-provisioned to Site {2}.".format(self.device_ip, already_provisioned_site, self.site_name)) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + self.status = "failed" + self.check_return_status() try: headers_payload = {"__persistbapioutput": "true"} @@ -1049,7 +1123,7 @@ def provision_wired_device(self, to_provisioning, to_force_provisioning): if status == "success": if not to_force_provisioning: self.result["changed"] = False - msg = "Device '{0}' is already provisioned.".format(self.validated_config.get("management_ip_address")) + msg = "Wired Device '{0}' is already provisioned.".format(self.validated_config.get("management_ip_address")) self.result['msg'] = msg self.result['response'] = msg self.log(msg, "INFO") @@ -1281,22 +1355,23 @@ def provision_wireless_device(self): site_exist, site_id = self.get_site_id(site_name) - primary_ap_location_site_id_list = [] - secondary_ap_location_site_id_list = [] + if self.compare_dnac_versions(self.get_ccc_version(), "2.3.7.6") <= 0: + primary_ap_location_site_id_list = [] + secondary_ap_location_site_id_list = [] - if primary_ap_location: - self.log("Processing primary access point locations", "INFO") - for primary_sites in primary_ap_location: - self.log("Retrieving site ID for primary location: {0}".format(primary_ap_location), "DEBUG") - site_exist, primary_ap_location_site_id = self.get_site_id(primary_sites) - primary_ap_location_site_id_list.append(primary_ap_location_site_id) + if primary_ap_location: + self.log("Processing primary access point locations", "INFO") + for primary_sites in primary_ap_location: + self.log("Retrieving site ID for primary location: {0}".format(primary_sites), "DEBUG") + site_exist, primary_ap_location_site_id = self.get_site_id(primary_sites) + primary_ap_location_site_id_list.append(primary_ap_location_site_id) - if secondary_ap_location: - self.log("Processing secondary access point locations", "INFO") - for secondary_sites in secondary_ap_location: - self.log("Retrieving site ID for secondary location: {0}".format(secondary_ap_location), "DEBUG") - site_exist, secondary_ap_location_site_id = self.get_site_id(secondary_sites) - secondary_ap_location_site_id_list.append(secondary_ap_location_site_id) + if secondary_ap_location: + self.log("Processing secondary access point locations", "INFO") + for secondary_sites in secondary_ap_location: + self.log("Retrieving site ID for secondary location: {0}".format(secondary_sites), "DEBUG") + site_exist, secondary_ap_location_site_id = self.get_site_id(secondary_sites) + secondary_ap_location_site_id_list.append(secondary_ap_location_site_id) if self.compare_dnac_versions(self.get_ccc_version(), "2.3.5.3") <= 0: self.log("Detected Catalyst Center version <= 2.3.5.3; using old provisioning method", "INFO") @@ -1323,6 +1398,7 @@ def provision_wireless_device(self): self.status = "failed" self.result['response'] = self.msg self.check_return_status() + else: self.log("Detected Catalyst Center version > 2.3.5.3; using new provisioning method", "INFO") self.log("Checking if device is assigned to the site", "INFO") @@ -1370,9 +1446,9 @@ def provision_wireless_device(self): self.log("Processing interfaces if they exist", "INFO") self.log("Building payload for wireless provisioning", "INFO") - if 'dynamic_interfaces' in prov_params: + if 'dynamicInterfaces' in prov_params: self.log("Processing dynamic interfaces", "INFO") - for interface in prov_params['dynamic_interfaces']: + for interface in prov_params['dynamicInterfaces']: cleaned_interface = {} for k, v in interface.items(): if v is not None: diff --git a/plugins/modules/rma_workflow_manager.py b/plugins/modules/rma_workflow_manager.py index 0d906e177b..9090f64765 100644 --- a/plugins/modules/rma_workflow_manager.py +++ b/plugins/modules/rma_workflow_manager.py @@ -54,10 +54,11 @@ default: False state: description: | - The desired state of the device replacement workflow. + The 'replaced' state is used to indicate the replacement of faulty network devices with + replacement network device in the workflow. type: str - choices: [ 'merged', 'deleted', 'replaced' ] - default: merged + choices: [ 'replaced' ] + default: replaced ccc_poll_interval: description: | The interval, in seconds, for polling Cisco Catalyst Center. @@ -309,7 +310,7 @@ class DeviceReplacement(DnacBase): def __init__(self, module): super().__init__(module) self.result["response"] = [] - self.supported_states = ["merged", "deleted", "replaced"] + self.supported_states = ["replaced"] self.payload = module.params self.keymap = {} self.faulty_device, self.replacement_device = [], [] @@ -1085,14 +1086,14 @@ def check_rma_task_status(self, task_id, success_message, error_prefix): timeout_interval = self.params.get('timeout_interval') while timeout_interval > 0: task_details = self.get_task_details(task_id) - - if task_details.get('response', {}).get("isError"): - error_message = task_details['response'].get("failureReason", "{0}: Task failed.".format(error_prefix)) + self.log(task_details) + if task_details.get("isError"): + error_message = task_details.get("failureReason", "{0}: Task failed.".format(error_prefix)) self.log(error_message, "ERROR") return {"status": "failed", "msg": error_message} - if 'response' in task_details and 'progress' in task_details['response']: - progress = task_details['response']['progress'].lower() + if 'progress' in task_details: + progress = task_details['progress'].lower() if 'successful' in progress: self.log(success_message, "INFO") @@ -1206,7 +1207,7 @@ def main(): 'timeout_interval': {'type': 'int', 'default': 100}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'validate_response_schema': {'type': 'bool', 'default': True}, - 'state': {'default': 'merged', 'choices': ['merged', 'deleted', 'replaced']} + 'state': {'default': 'replaced', 'choices': ['replaced']} } module = AnsibleModule( argument_spec=device_replacement_spec, diff --git a/plugins/modules/sda_fabric_devices_workflow_manager.py b/plugins/modules/sda_fabric_devices_workflow_manager.py index 9daff687fd..536661abb6 100644 --- a/plugins/modules/sda_fabric_devices_workflow_manager.py +++ b/plugins/modules/sda_fabric_devices_workflow_manager.py @@ -88,6 +88,7 @@ managing traffic entering or exiting the SDA environment. - WIRELESS_CONTROLLER_NODE - Manages and controls wireless access points and devices within the network. + - For 'WIRELESS_CONTROLLER_NODE', the check for the provisioning status will be added in 2.3.7.6 SDK. choices: [CONTROL_PLANE_NODE, EDGE_NODE, BORDER_NODE, WIRELESS_CONTROLLER_NODE] type: list elements: str @@ -971,7 +972,7 @@ def get_obj_params(self, get_object): if get_object == "fabricDevices": obj_params = [ ("borderTypes", "borderTypes"), - ("layer3Settings", "layer3Settings"), + ("borderDeviceSettings", "borderDeviceSettings"), ] elif get_object == "fabricSdaL3Handoff": obj_params = [ @@ -1043,22 +1044,22 @@ def get_transit_id_from_name(self, transit_name): ) return transit_id - def get_device_id_from_ip(self, device_ip): + def get_device_details_from_ip(self, device_ip): """ - Get the network device ID from the network device IP. + Get the network device details from the network device IP. Parameters: device_ip (str): The IP address of the network device. Returns: - device_id (str or None): The ID of the network device. None, if the device doesnot exist. + device_details (dict or None): The details of the network device. None, if the device doesnot exist. Description: Call the API 'get_device_list' by setting the 'management_ip_address' field with the given IP address. - If the response is not empty, fetch the Id and return. Else, return None. + If the response is not empty, return the device details. Else, return None. """ - self.log("Starting to get device ID for device IP: '{ip}'.".format(ip=device_ip), "DEBUG") - device_id = None + self.log("Starting to get device details for device IP: '{ip}'.".format(ip=device_ip), "DEBUG") + device_details = None try: device_details = self.dnac._exec( family="devices", @@ -1077,17 +1078,8 @@ def get_device_id_from_ip(self, device_ip): "There is no device with the IP address '{ip_address}'." .format(ip_address=device_ip), "DEBUG" ) - return device_id + return device_details - self.log( - "The device with the IP {ip} is a valid network device IP." - .format(ip=device_ip), "DEBUG" - ) - device_id = device_details[0].get("id") - self.log( - "Device ID found: '{id}' for device IP: '{ip}'." - .format(id=device_id, ip=device_ip), "DEBUG" - ) except Exception as msg: self.msg = ( "Exception occured while running the API 'get_device_list': {msg}" @@ -1098,9 +1090,9 @@ def get_device_id_from_ip(self, device_ip): return self.check_return_status() self.log( - "Returning device ID: '{id}'.".format(id=device_id), "DEBUG" + "Returning device details: '{details}'.".format(details=device_details), "DEBUG" ) - return device_id + return device_details def check_valid_virtual_network_name(self, virtual_network_name): """ @@ -1373,8 +1365,8 @@ def check_device_is_provisioned(self, fabric_device_ip, device_id, site_id, site provisioned_device_details = provisioned_device_details.get("response") if not provisioned_device_details: self.msg = ( - "The network device with the IP address '{device_ip}' is not provisioned." - .format(device_ip=fabric_device_ip) + "The network device with the IP address '{device_ip}' is not provisioned to the site '{site_name}'." + .format(device_ip=fabric_device_ip, site_name=site_name) ) self.log(self.msg, "ERROR") self.status = "failed" @@ -1465,7 +1457,7 @@ def l2_handoff_exists(self, fabric_id, device_id, internal_vlan_id, interface_na if not isinstance(all_l2_handoff_details, dict): self.msg = "Failed to retrieve the L2 Handoff details - Response is not a dictionary" - self.log(self.msg, "CRITICAL") + self.log(str(self.msg), "CRITICAL") self.status = "failed" return self.check_return_status() @@ -1477,10 +1469,12 @@ def l2_handoff_exists(self, fabric_id, device_id, internal_vlan_id, interface_na ) break - l2_handoff_details = next(item for item in all_l2_handoff_details - if item.get("internalVlanId") == internal_vlan_id and - item.get("interfaceName") == interface_name) - l2_handoff_details = get_dict_result(all_l2_handoff_details, "internalVlanId", internal_vlan_id) + l2_handoff_details = None + for item in all_l2_handoff_details: + if item.get("internalVlanId") == internal_vlan_id and item.get("interfaceName") == interface_name: + l2_handoff_details = item + break + if l2_handoff_details: self.log( "The L2 handoff details with the internal VLAN Id: {details}" @@ -1862,7 +1856,7 @@ def process_layer2_handoff(self, fabric_devices_info, layer2_handoff, fabric_sit network_device_id, internal_vlan_id, interface_name) - fabric_devices_info.get("l2_handoff_ids").append(l2_handoff_id) + fabric_devices_info.get("l2_handoff_details").append(l2_handoff_id) self.log("Successfully proccessed the L2 Handoff information.", "DEBUG") return fabric_devices_info @@ -2046,7 +2040,7 @@ def get_have_fabric_devices(self, fabric_devices): fabric_devices_info = { "exists": False, "device_details": None, - "l2_handoff_ids": [], + "l2_handoff_details": [], "sda_l3_handoff_details": None, "ip_l3_handoff_details": [], "id": None, @@ -2056,10 +2050,6 @@ def get_have_fabric_devices(self, fabric_devices): } # Fabric device IP is mandatory for this workflow - self.log( - "Checking if device '{device_ip}' exists in fabric '{fabric_name}'." - .format(device_ip=fabric_device_ip, fabric_name=fabric_name), "INFO" - ) fabric_device_ip = item.get("device_ip") if not fabric_device_ip: self.msg = ( @@ -2070,12 +2060,16 @@ def get_have_fabric_devices(self, fabric_devices): self.status = "failed" return self + self.log( + "Checking if device '{device_ip}' exists in fabric '{fabric_name}'." + .format(device_ip=fabric_device_ip, fabric_name=fabric_name), "INFO" + ) self.log( "Fetching network device ID for IP '{device_ip}'." .format(device_ip=fabric_device_ip), "INFO" ) - network_device_id = self.get_device_id_from_ip(fabric_device_ip) - if not network_device_id: + network_device_details = self.get_device_details_from_ip(fabric_device_ip) + if not network_device_details: self.msg = ( "The 'device_ip' '{ip}' in 'device_config' is not a valid IP under the fabric '{fabric_name}'." .format(ip=fabric_device_ip, fabric_name=fabric_name) @@ -2084,13 +2078,31 @@ def get_have_fabric_devices(self, fabric_devices): self.status = "failed" return self + self.log( + "The device with the IP {ip} is a valid network device IP." + .format(ip=fabric_device_ip), "DEBUG" + ) + network_device_id = network_device_details[0].get("id") self.log( "Obtained network device ID: {network_device_id}." .format(network_device_id=network_device_id), "DEBUG" ) + family_name = network_device_details[0].get("family") + if family_name != "Wireless Controller": + self.log( + "The device with the IP '{ip}' is not a Wireless Controller, " + "proceeding with provisioning checks." + ) + self.check_device_is_provisioned(fabric_device_ip, + network_device_id, + site_id, + fabric_name).check_return_status() + else: + self.log( + "The device with the IP '{ip}' is a Wireless Controller, " + "skipping provisioning checks." + ) - # The device should be provisioned to the site - self.check_device_is_provisioned(fabric_device_ip, network_device_id, site_id, fabric_name).check_return_status() delete_fabric_device = item.get("delete_fabric_device") if delete_fabric_device is None: delete_fabric_device = False @@ -2191,6 +2203,72 @@ def get_have(self, config): self.status = "success" return self + def validate_local_autonomous_system_number(self, local_autonomous_system_number, device_ip): + """ + Validate the local autonomous system number for the border settings. + Set the status and the msg before returning from the API + Check the return value of the API with check_return_status() + + Parameters: + local_autonomous_system_number (str): Local Autonomous System number of the border device. + device_ip (str): The IP address of the network device. + Description: + The Local Autonomous System number can of two format. One is from 1 to 4294967295. + The other one can be of dot format. from 1.0 to 65535.65535. + Find '.' is in the 'local_autonomous_system_number'. If yes split it into two part + and check if the left part is between 1 to 65535 and the right part is from + 0 to 65535. If the '.' is not present in the 'local_autonomous_system_number', + check if the value is between 1 to 4294967295. + """ + + try: + str_asn = str(local_autonomous_system_number) + if "." in str_asn: + + # Split the input into two parts + parts = str_asn.split(".") + if len(parts) == 2: + first_part = int(parts[0]) + second_part = int(parts[1]) + + # Validate the range for both parts + if 1 <= first_part <= 65535 and 0 <= second_part <= 65535: + self.log("Input is valid in the format 1.0 to 65535.65535", "INFO") + else: + self.msg = ( + "The 'local_autonomous_system_number' should be in the range '1.0' to '65535.65535' for the device '{ip}'." + .format(ip=device_ip) + ) + self.status = "failed" + return self.check_return_status() + else: + self.msg = ( + "The 'local_autonomous_system_number' should contain one '.' and two numeric parts for the device '{ip}'." + .format(ip=device_ip) + ) + self.status = "failed" + return self.check_return_status() + else: + local_autonomous_system_number = int(local_autonomous_system_number) + if not 1 <= local_autonomous_system_number <= 4294967295: + self.msg = ( + "The 'local_autonomous_system_number' should be from 1 to 4294967295 for the device '{ip}'." + .format(ip=device_ip) + ) + self.status = "failed" + return self.check_return_status() + + except ValueError: + self.msg = ( + "The 'local_autonomous_system_number' should contain only digits 0-9 for the device '{ip}'." + .format(ip=device_ip) + ) + self.status = "failed" + return self.check_return_status() + + self.log("The 'local_autonomous_system_number' is successfully validated.") + return + def get_device_params(self, fabric_id, network_id, device_details, config_index): """ Get the SDA fabric devices detail along with the border @@ -2227,18 +2305,18 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) # retrieved from the Cisco Catalyst Center, we will use it have_device_details = self.have.get("fabric_devices")[config_index].get("device_details") self.log("Existing device details found: {device_details}".format(device_details=have_device_details), "DEBUG") - have_device_exists = None + have_device_exists = False if have_device_details: have_device_exists = self.have.get("fabric_devices")[config_index] \ - .get("device_details").get("exists") + .get("exists") self.log("Device exists status: {device_exists}".format(device_exists=have_device_exists), "DEBUG") # Device IP and the Fabric name is mandatory and cannot be fetched from the Cisco Catalyst Center device_roles = device_details.get("device_roles") self.log("Device roles provided: {roles}".format(roles=device_roles), "DEBUG") - if not device_roles: - if not have_device_exists: + if not have_device_exists: + if not device_roles: self.msg = ( "The parameter 'device_roles is mandatory under 'device_config' " "for the device with IP '{ip}'.".format(ip=device_ip) @@ -2246,22 +2324,21 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) self.status = "failed" return self.check_return_status() - device_roles = have_device_details.get("deviceRoles") - else: device_roles_list = ["CONTROL_PLANE_NODE", "EDGE_NODE", "BORDER_NODE", "WIRELESS_CONTROLLER_NODE"] - for item in device_roles: - if item not in device_roles_list: - self.msg = ( - "The value '{item}' in 'device_roles' for the IP '{ip}' should be in the list '{roles_list}'." - .format(item=item, ip=device_ip, roles_list=device_roles_list) - ) - self.status = "failed" - return self.check_return_status() + if device_roles is not None: + for item in device_roles: + if item not in device_roles_list: + self.msg = ( + "The value '{item}' in 'device_roles' for the IP '{ip}' should be in the list '{roles_list}'." + .format(item=item, ip=device_ip, roles_list=device_roles_list) + ) + self.status = "failed" + return self.check_return_status() # The role of the device cannot be updated - if have_device_exists and device_roles != have_device_details.get("deviceRoles"): + if device_roles and sorted(device_roles) != sorted(have_device_details.get("deviceRoles")): self.msg = ( "The parameter 'device_roles' cannot be updated in the device with IP '{ip}'." .format(ip=device_ip) @@ -2269,6 +2346,9 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) self.status = "failed" return self.check_return_status() + if not device_roles: + device_roles = have_device_details.get("deviceRoles") + device_info.update({ "deviceRoles": device_roles }) @@ -2302,8 +2382,16 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) border_device_settings = {} border_types = [] layer3_settings = borders_settings.get("layer3_settings") + layer3_handoff_ip_transit = borders_settings.get("layer3_handoff_ip_transit") + layer3_handoff_sda_transit = borders_settings.get("layer3_handoff_sda_transit") if layer3_settings: border_types.append("LAYER_3") + elif layer3_handoff_ip_transit: + border_types.append("LAYER_3") + elif layer3_handoff_sda_transit: + border_types.append("LAYER_3") + elif "BORDER_NODE" in device_roles: + border_types.append("LAYER_3") l2_handoff = borders_settings.get("layer2_handoff") if l2_handoff: @@ -2330,9 +2418,17 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) have_layer3_settings = have_border_settings.get("layer3Settings") if "LAYER_3" in border_types: + if not (layer3_settings or have_layer3_settings): + self.msg = ( + "The parameter 'border_settings' is mandatory when the 'device_roles' has 'BORDER_NODE' " + "for the device {ip}.".format(ip=device_ip) + ) + self.status = "failed" + return self.check_return_status() + local_autonomous_system_number = layer3_settings.get("local_autonomous_system_number") self.log("Local AS number: {asn_number}".format(asn_number=local_autonomous_system_number), "DEBUG") - if not local_autonomous_system_number: + if local_autonomous_system_number is None: if have_layer3_settings: local_autonomous_system_number = have_layer3_settings.get("localAutonomousSystemNumber") else: @@ -2343,24 +2439,7 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) self.status = "failed" return self.check_return_status() - try: - local_autonomous_system_number = int(local_autonomous_system_number) - if not 1 <= local_autonomous_system_number <= 4294967295: - self.msg = ( - "The 'local_autonomous_system_number' should be from 1 to 4294967295 for the device '{ip}'." - .format(ip=device_ip) - ) - self.status = "failed" - return self.check_return_status() - - except ValueError: - self.msg = ( - "The 'local_autonomous_system_number' should contain only digits 0-9 for the device '{ip}'." - .format(ip=device_ip) - ) - self.status = "failed" - return self.check_return_status() - + self.validate_local_autonomous_system_number(local_autonomous_system_number, device_ip) self.log( "Successfully validated 'local_autonomous_system_number': {asn_number}" .format(asn_number=local_autonomous_system_number), "DEBUG" @@ -2387,12 +2466,8 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) if not border_priority: if have_layer3_settings: have_border_priority = have_layer3_settings.get("borderPriority") - if have_border_priority: + if have_border_priority and have_border_priority != 10: border_priority = have_border_priority - else: - border_priority = 10 - else: - border_priority = 10 else: try: border_priority = int(border_priority) @@ -2418,12 +2493,8 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) if not prepend_autonomous_system_count: if have_layer3_settings: have_prepend_autonomous_system_count = have_layer3_settings.get("prependAutonomousSystemCount") - if have_prepend_autonomous_system_count: + if have_prepend_autonomous_system_count and have_prepend_autonomous_system_count != 0: prepend_autonomous_system_count = have_prepend_autonomous_system_count - else: - prepend_autonomous_system_count = 0 - else: - prepend_autonomous_system_count = 0 else: try: prepend_autonomous_system_count = int(prepend_autonomous_system_count) @@ -2445,13 +2516,20 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) border_device_settings.update({ "layer3Settings": { - "localAutonomousSystemNumber": local_autonomous_system_number, + "localAutonomousSystemNumber": str(local_autonomous_system_number), "isDefaultExit": is_default_exit, "importExternalRoutes": import_external_routes, - "borderPriority": border_priority, - "prependAutonomousSystemCount": prepend_autonomous_system_count, } }) + if border_priority: + border_device_settings.get("layer3Settings").update({ + "borderPriority": border_priority, + }) + + if prepend_autonomous_system_count: + border_device_settings.get("layer3Settings").update({ + "prependAutonomousSystemCount": prepend_autonomous_system_count, + }) device_info.update({ "borderDeviceSettings" : border_device_settings @@ -2539,6 +2617,57 @@ def validate_vlan_fields(self, internal_vlan_id, external_vlan_id, device_ip): return None + def check_transit_type(self, transit_id): + """ + Check whether the given transit id is LISP/PUB SUB or LISP/BGP. + + Parameters: + transit_id (str): The id of the transit network. + Returns: + is_transit_pub_sub (bool): Returns True, if the transit type is 'SDA_LISP_PUB_SUB_TRANSIT'. Else, False. + Description: + Calls the 'get_transit_networks' API by setting the 'id' and 'type' fields + with the given transit id and 'SDA_LISP_PUB_SUB_TRANSIT'. + If the response is valid, fetch the Id and return True, otherwise False. + """ + + self.log("Fetching transit type for transit ID: '{id}'".format(id=transit_id), "DEBUG") + is_transit_pub_sub = False + try: + transit_details = self.dnac._exec( + family="sda", + function="get_transit_networks", + params={ + "id": transit_id, + "type": "SDA_LISP_PUB_SUB_TRANSIT" + }, + ) + + # If the SDK returns no response, then the transit doesnot exist with type 'SDA_LISP_PUB_SUB_TRANSIT' + transit_details = transit_details.get("response") + if not transit_details: + self.log( + "There is no transit network with the id '{id}' with transit type 'SDA_LISP_PUB_SUB_TRANSIT'." + .format(id=transit_id), "DEBUG" + ) + return is_transit_pub_sub + + self.log( + "Transit network found with ID: '{id}' and type 'SDA_LISP_PUB_SUB_TRANSIT'." + .format(id=transit_id), "DEBUG" + ) + is_transit_pub_sub = True + except Exception as msg: + self.msg = ( + "Exception occured while running the API 'get_transit_networks': {msg}" + .format(msg=msg) + ) + self.log(self.msg, "CRITICAL") + self.status = "failed" + return self.check_return_status() + + return is_transit_pub_sub + def get_l2_handoff_params(self, fabric_id, network_id, device_details, device_config_index): """ Get the L2 Handoff of the SDA fabric devices information from playbook. @@ -2698,20 +2827,26 @@ def get_sda_l3_handoff_params(self, fabric_id, network_id, device_details, devic "Transit ID for the transit name {name}: {id}" .format(name=transit_name, id=transit_id) ) + + is_transit_pub_sub = self.check_transit_type(transit_id) + self.log( + "The transit type is 'LISP/PUB SUB': {is_transit_pub_sub}" + .format(is_transit_pub_sub=is_transit_pub_sub) + ) connected_to_internet = layer3_handoff_sda_transit.get("connected_to_internet") - if not connected_to_internet: + if connected_to_internet is None: if is_sda_l3_handoff_exists: connected_to_internet = have_sda_l3_handoff.get("connectedToInternet") else: connected_to_internet = False - self.log( - "Connected to internet for device IP {device_ip}: {connected_to_internet}" - .format(device_ip=device_ip, connected_to_internet=connected_to_internet), "DEBUG" - ) + self.log( + "Connected to internet for device IP {device_ip}: {connected_to_internet}" + .format(device_ip=device_ip, connected_to_internet=connected_to_internet), "DEBUG" + ) is_multicast_over_transit_enabled = layer3_handoff_sda_transit.get("is_multicast_over_transit_enabled") - if not is_multicast_over_transit_enabled: + if is_multicast_over_transit_enabled is None: if is_sda_l3_handoff_exists: is_multicast_over_transit_enabled = have_sda_l3_handoff.get("isMulticastOverTransitEnabled") else: @@ -2794,13 +2929,17 @@ def get_sda_l3_handoff_params(self, fabric_id, network_id, device_details, devic return self.check_return_status() sda_l3_handoff_info.update({ - "interfaceName": transit_id, - "affinityIdPrime": affinity_id_prime, - "affinityIdDecider": affinity_id_decider, + "transitNetworkId": transit_id, "connectedToInternet": connected_to_internet, - "isMulticastOverTransitEnabled": is_multicast_over_transit_enabled, }) + if is_transit_pub_sub: + sda_l3_handoff_info.update({ + "affinityIdPrime": affinity_id_prime, + "affinityIdDecider": affinity_id_decider, + "isMulticastOverTransitEnabled": is_multicast_over_transit_enabled, + }) + self.log( "Successfully retrieved L3 handoff parameters for device IP: {device_ip}" .format(device_ip=device_ip) @@ -2906,7 +3045,7 @@ def validate_layer3_handoff_ip_transit(self, item, device_ip, is_ip_l3_handoff_e self.log(self.msg, "ERROR") return (None, None, None, None, None, False) - if not is_ip_l3_handoff_exists: + if is_ip_l3_handoff_exists: if not (virtual_network_name and vlan_id): self.msg = ( "The 'virtual_network_name' or 'vlan_id' is mandatory under 'layer3_handoff_ip_transit' " @@ -2921,14 +3060,15 @@ def validate_layer3_handoff_ip_transit(self, item, device_ip, is_ip_l3_handoff_e elif vlan_id and (not virtual_network_name): virtual_network_name = have_ip_l3_handoff.get("virtualNetworkName") else: - self.msg = ( - "The 'virtual_network_name' and 'vlan_id' are invalid under 'layer3_handoff_ip_transit' for " - "adding the Layer 3 Handoff with IP transit in the device with IP '{ip}'" - .format(ip=device_ip) - ) - self.status = "failed" - self.log(self.msg, "ERROR") - return (None, None, None, None, None, False) + if not (virtual_network_name and vlan_id): + self.msg = ( + "The 'virtual_network_name' and 'vlan_id' are mandatory under 'layer3_handoff_ip_transit' for " + "adding the Layer 3 Handoff with IP transit in the device with IP '{ip}'" + .format(ip=device_ip) + ) + self.status = "failed" + self.log(self.msg, "ERROR") + return (None, None, None, None, None, False) tcp_mss_adjustment = item.get("tcp_mss_adjustment") if not tcp_mss_adjustment: @@ -3014,14 +3154,14 @@ def get_ip_l3_handoff_params(self, fabric_id, network_id, device_details, device .get("ip_l3_handoff_details") if have_ip_l3_handoff and have_ip_l3_handoff[l3_ip_handoff_index]: is_ip_l3_handoff_exists = True + self.log( + "Existing IP L3 handoff found for index {index}: {details}" + .format(index=l3_ip_handoff_index, details=have_ip_l3_handoff[l3_ip_handoff_index]), "DEBUG" + ) - self.log( - "Existing IP L3 handoff found for index {index}: {details}" - .format(index=l3_ip_handoff_index, details=have_ip_l3_handoff[l3_ip_handoff_index]), "DEBUG" - ) (transit_id, interface_name, virtual_network_name, vlan_id, tcp_mss_adjustment, is_valid) = \ self.validate_layer3_handoff_ip_transit( - item, device_details.get("device_ip"), l3_ip_handoff_index, have_ip_l3_handoff + item, device_details.get("device_ip"), is_ip_l3_handoff_exists, have_ip_l3_handoff ) if not is_valid: @@ -3045,10 +3185,10 @@ def get_ip_l3_handoff_params(self, fabric_id, network_id, device_details, device # If the fabric device is avaiable, then fetch the local and remote IP addresses if is_ip_l3_handoff_exists: - local_ip_address = have_ip_l3_handoff.get("localIpAddress") - remote_ip_address = have_ip_l3_handoff.get("remoteIpAddress") - local_ipv6_address = have_ip_l3_handoff.get("localIpv6Address") - remote_ipv6_address = have_ip_l3_handoff.get("remoteIpv6Address") + local_ip_address = have_ip_l3_handoff[l3_ip_handoff_index].get("localIpAddress") + remote_ip_address = have_ip_l3_handoff[l3_ip_handoff_index].get("remoteIpAddress") + local_ipv6_address = have_ip_l3_handoff[l3_ip_handoff_index].get("localIpv6Address") + remote_ipv6_address = have_ip_l3_handoff[l3_ip_handoff_index].get("remoteIpv6Address") l3_ip_handoff.update({ "localIpAddress": local_ip_address, "remoteIpAddress": remote_ip_address, @@ -3098,6 +3238,13 @@ def get_ip_l3_handoff_params(self, fabric_id, network_id, device_details, device "localIpv6Address": local_ipv6_address, "remoteIpv6Address": remote_ipv6_address }) + elif local_ipv6_address or remote_ipv6_address: + self.msg = ( + "If IPv6 addresses need to added. Please provide both local and remote IPv6 address " + "for the device '{ip}".format(ip=device_ip) + ) + self.status = "failed" + return self.check_return_status() ip_l3_handoff_info.append(l3_ip_handoff) @@ -3397,6 +3544,9 @@ def update_sda_l3_handoff(self, have_sda_l3_handoff, want_sda_l3_handoff, # SDA L3 Handoff requires an update or not try: + want_sda_l3_handoff.update({ + "id": have_sda_l3_handoff.get("id") + }) payload = {"payload": [want_sda_l3_handoff]} task_name = "update_fabric_devices_layer3_handoffs_with_sda_transit" self.log( @@ -3491,7 +3641,7 @@ def update_ip_l3_handoff(self, have_ip_l3_handoff, want_ip_l3_handoff, ) # Check for the IP L3 Handoff existence - if not have_ip_l3_handoff[ip_l3_handoff_index]: + if not (have_ip_l3_handoff and have_ip_l3_handoff[ip_l3_handoff_index]): create_ip_l3_handoff.append(item) self.log( "Handoff {index} requires creation on device {ip}.".format( @@ -3509,6 +3659,7 @@ def update_ip_l3_handoff(self, have_ip_l3_handoff, want_ip_l3_handoff, ), "DEBUG" ) + item.update({"id": have_ip_l3_handoff[ip_l3_handoff_index].get("id")}) update_ip_l3_handoff.append(item) # If both the list are empty, then not update is required @@ -3682,6 +3833,7 @@ def update_fabric_devices(self, fabric_devices): device_ip: {} }) result_fabric_device_response = self.response[0].get("response").get(fabric_name).get(device_ip) + self.log("hi") result_fabric_device_msg = self.response[0].get("msg").get(fabric_name).get(device_ip) have_fabric_device = self.have.get("fabric_devices")[fabric_device_index] want_fabric_device = self.want.get("fabric_devices")[fabric_device_index] @@ -3880,6 +4032,69 @@ def get_diff_merged(self, config): return self + def prioritize_device_deletion(self, device_config): + """ + Prioritize the device config so that the device with the role 'CONTROL_PLANE_NODE' + is deleted last, and devices that don't exist are handled first. + + Parameters: + device_config (list): Devices config details provided by the user in the playbook. + Returns: + updated_device_config (list): Reordered device config details. + Description: + For each device in the config, check whether it exists in the Cisco Catalyst Center. + If the device doesn't exist, prepend it to the list. If it has a role 'CONTROL_PLANE_NODE', + append it to ensure it is processed last. Update self.have['fabric_details'] with the new + order and return the updated config. + """ + + fabric_device_index = -1 + updated_device_config = [] + update_have = [] + self.log("Starting to reorder devices based on their existence and role.", "DEBUG") + self.log("Input device_config: {device_config}".format(device_config=device_config), "DEBUG") + for item in device_config: + fabric_device_index += 1 + device_ip = item.get("device_ip") + self.log("Processing device with IP: {ip}".format(ip=device_ip)) + have_device_details = self.have.get("fabric_devices")[fabric_device_index] + exists = have_device_details.get("exists") + if not exists: + self.log( + "The device with IP address '{ip}' is not available in the Cisco Catalyst Center." + .format(ip=device_ip) + ) + updated_device_config = [item] + updated_device_config + update_have = [have_device_details] + update_have + continue + + device_roles = have_device_details.get("device_details").get("deviceRoles") + if "CONTROL_PLANE_NODE" in device_roles: + self.log( + "Device with IP '{ip}' has role 'CONTROL_PLANE_NODE', appending to the end." + .format(ip=device_ip) + ) + updated_device_config.append(item) + update_have.append(have_device_details) + continue + + self.log( + "Device with IP '{ip}' role '{device_roles}', prepending." + .format(ip=device_ip, device_roles=device_roles) + ) + updated_device_config.insert(0, item) + update_have.insert(0, have_device_details) + + self.have.update({ + "fabric_devices": update_have + }) + self.log( + "Updated device_config for deletion: {updated_config}" + .format(updated_config=updated_device_config) + ) + + return updated_device_config + def delete_l2_handoff(self, have_l2_handoff, device_ip, result_fabric_device_response, result_fabric_device_msg): @@ -3924,8 +4139,7 @@ def delete_l2_handoff(self, have_l2_handoff, device_ip, "Non-existing L2 Handoffs: {non_existing_list}" .format(non_existing_list=non_existing_l2_handoff), "DEBUG" ) - for item in delete_l2_handoff: - id = item.get("id") + for id in delete_l2_handoff: try: payload = {"id": id} task_name = "delete_fabric_device_layer2_handoff_by_id" @@ -4175,7 +4389,8 @@ def delete_fabric_devices(self, fabric_devices): "Starting deletion of fabric devices under fabric '{fabric_name}'" .format(fabric_name=fabric_name), "DEBUG" ) - for item in device_config: + updated_device_config = self.prioritize_device_deletion(device_config) + for item in updated_device_config: fabric_device_index += 1 device_ip = item.get("device_ip") self.response[0].get("response").get(fabric_name).update({ @@ -4194,25 +4409,38 @@ def delete_fabric_devices(self, fabric_devices): result_fabric_device_msg = self.response[0].get("msg").get(fabric_name).get(device_ip) have_fabric_device = self.have.get("fabric_devices")[fabric_device_index] have_l2_handoff = have_fabric_device.get("l2_handoff_details") - if item.get("l2_handoff_details"): + if have_l2_handoff: self.log("Deleting L2 Handoff for device '{device_ip}'".format(device_ip=device_ip), "DEBUG") self.delete_l2_handoff(have_l2_handoff, device_ip, result_fabric_device_response, result_fabric_device_msg).check_return_status() - + else: + result_fabric_device_msg.update({ + "l3_ip_handoff": "IP L3 Handoff doesnot found in the Cisco Catalyst Center." + }) have_sda_l3_handoff = have_fabric_device.get("sda_l3_handoff_details") - if item.get("sda_l3_handoff_details"): + if have_sda_l3_handoff: self.log("Deleting SDA L3 Handoff for device '{device_ip}'".format(device_ip=device_ip), "DEBUG") self.delete_sda_l3_handoff(have_sda_l3_handoff, device_ip, result_fabric_device_response, result_fabric_device_msg).check_return_status() + else: + result_fabric_device_msg.update({ + "l3_sda_handoff": "SDA L3 Handoff doesnot found in the Cisco Catalyst Center." + }) have_ip_l3_handoff = have_fabric_device.get("ip_l3_handoff_details") - if item.get("ip_l3_handoff_details"): + + if have_ip_l3_handoff: self.log("Deleting IP L3 Handoff for device '{device_ip}'".format(device_ip=device_ip), "DEBUG") self.delete_ip_l3_handoff(have_ip_l3_handoff, device_ip, result_fabric_device_response, result_fabric_device_msg).check_return_status() + else: + result_fabric_device_msg.update({ + "l2_handoff": "L2 Handoff doesnot found in the Cisco Catalyst Center." + }) + delete_fabric_device = have_fabric_device.get("delete_fabric_device") device_exists = have_fabric_device.get("exists") @@ -4266,7 +4494,7 @@ def delete_fabric_devices(self, fabric_devices): "response": self.response }) self.msg = "The deletion of devices L2 Handoff, L3 Handoff with IP and SDA transit is successful." - self.log(str(self.msg, "DEBUG")) + self.log(str(self.msg), "DEBUG") self.status = "success" return self @@ -4309,8 +4537,8 @@ def verify_ip_l3_handoff(self, device_ip, have_l3_ip, want_l3_ip): ip_l3_handoff_index = -1 for item in want_l3_ip: ip_l3_handoff_index += 1 - if self.requires_update(have_l3_ip[ip_l3_handoff_index], - item, self.fabric_l3_handoff_ip_obj_params): + if not self.requires_update(have_l3_ip[ip_l3_handoff_index], + item, self.fabric_l3_handoff_ip_obj_params): self.msg = ( "The L3 Handoff for IP transit config for the device '{ip}' is still not " "applied to the Cisco Catalyst Center.".format(ip=device_ip) diff --git a/plugins/modules/sda_fabric_sites_zones_workflow_manager.py b/plugins/modules/sda_fabric_sites_zones_workflow_manager.py index d1e063d0db..fd6c0de572 100644 --- a/plugins/modules/sda_fabric_sites_zones_workflow_manager.py +++ b/plugins/modules/sda_fabric_sites_zones_workflow_manager.py @@ -145,8 +145,8 @@ dnac_log: False state: merged config: - - fabric sites: - site_name: "Global/Test_SDA/Bld1" + - fabric_sites: + - site_name: "Global/Test_SDA/Bld1" authentication_profile: "Closed Authentication" is_pub_sub_enabled: False @@ -163,8 +163,8 @@ dnac_log: False state: merged config: - - fabric sites: - site_name: "Global/Test_SDA/Bld1" + - fabric_sites: + - site_name: "Global/Test_SDA/Bld1" authentication_profile: "Open Authentication" - name: Update a fabric zone for SDA with the specified name. @@ -180,8 +180,8 @@ dnac_log: False state: merged config: - - fabric sites: - site_name: "Global/Test_SDA/Bld1/Floor1" + - fabric_sites: + - site_name: "Global/Test_SDA/Bld1/Floor1" fabric_type: "fabric_zone" authentication_profile: "Closed Authentication" @@ -198,8 +198,8 @@ dnac_log: False state: merged config: - - fabric sites: - site_name: "Global/Test_SDA/Bld1/Floor1" + - fabric_sites: + - site_name: "Global/Test_SDA/Bld1/Floor1" fabric_type: "fabric_zone" authentication_profile: "Open Authentication" @@ -217,7 +217,7 @@ state: merged config: - fabric_sites: - site_name: "Global/Test_SDA/Bld1" + - site_name: "Global/Test_SDA/Bld1" fabric_type: "fabric_zone" authentication_profile: "Open Authentication" is_pub_sub_enabled: False @@ -241,7 +241,7 @@ state: deleted config: - fabric_sites: - site_name: "Global/Test_SDA/Bld1" + - site_name: "Global/Test_SDA/Bld1" - name: Deleting/removing fabric zone from sda from Cisco Catalyst Center cisco.dnac.sda_fabric_sites_zones_workflow_manager: @@ -257,7 +257,7 @@ state: deleted config: - fabric_sites: - site_name: "Global/Test_SDA/Bld1/Floor1" + - site_name: "Global/Test_SDA/Bld1/Floor1" fabric_type: "fabric_zone" """ @@ -326,7 +326,7 @@ def validate_input(self): 'is_pub_sub_enabled': {'type': 'bool', 'default': False}, 'update_authentication_profile': { 'elements': 'dict', - 'site_name_hierarchy': {'type': 'str'}, + 'site_name': {'type': 'str'}, 'authentication_profile': {'type': 'str'}, 'authentication_order': {'type': 'str'}, 'dot1x_fallback_timeout': {'type': 'int'}, @@ -622,16 +622,14 @@ def create_fabric_site(self, site): fabric_site_payload = [] site_name = site.get("site_name") auth_profile = site.get("authentication_profile") + if not auth_profile: - self.status = "failed" self.msg = ( "Required parameter 'authentication_profile'is missing needed for creation of fabric sites in Cisco Catalyst Center. " "Please provide one of the following authentication_profile ['Closed Authentication', 'Low Impact'" ", 'No Authentication', 'Open Authentication'] in the playbook." ) - self.log(self.msg, "ERROR") - self.result["response"] = self.msg - return self + self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status() site_payload = { "siteId": self.get_site_id(site_name), @@ -639,50 +637,22 @@ def create_fabric_site(self, site): "isPubSubEnabled": site.get("is_pub_sub_enabled", False) } fabric_site_payload.append(site_payload) - self.log("Requested payload for creating fabric site '{0}' is: {1}".format(site_name, str(site_payload)), "INFO") - - response = self.dnac._exec( - family="sda", - function='add_fabric_site', - op_modifies=True, - params={'payload': fabric_site_payload} - ) - self.log("Received API response from 'add_fabric_site' for the site {0}: {1}".format(site_name, str(response)), "DEBUG") - response = response.get("response") + task_name = "add_fabric_site" + payload = {"payload": fabric_site_payload} + task_id = self.get_taskid_post_api_call("sda", task_name, payload) - if not response: - self.status = "failed" - self.msg = "No response received from 'add_fabric_site' API, task ID not retrieved." - self.log(self.msg, "ERROR") + if not task_id: + self.msg = "Unable to retrieve the task_id for the task '{0}'.".format(task_name) + self.set_operation_result("failed", False, self.msg, "ERROR") return self - task_id = response.get("taskId") - - while True: - task_details = self.get_task_details(task_id) - - if task_details.get("isError"): - self.status = "failed" - failure_reason = task_details.get("failureReason") - if failure_reason: - self.msg = "Failed to create the Fabric site '{0}' due to {1}.".format(site_name, failure_reason) - else: - self.msg = "Failed to create the Fabric site '{0}'.".format(site_name) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - break - elif task_details.get("endTime") and "workflow_id" in task_details.get("data"): - self.status = "success" - self.create_site.append(site_name) - self.log("Fabric site '{0}' created successfully in the Cisco Catalyst Center".format(site_name), "INFO") - break - - time.sleep(1) + success_msg = "Fabric site '{0}' created successfully in the Cisco Catalyst Center".format(site_name) + self.get_task_status_from_tasks_by_id(task_id, task_name, success_msg) + self.create_site.append(site_name) except Exception as e: - self.status = "failed" self.msg = "An exception occured while creating the fabric site '{0}' in Cisco Catalyst Center: {1}".format(site_name, str(e)) - self.log(self.msg, "ERROR") + self.set_operation_result("failed", False, self.msg, "ERROR") return self @@ -754,43 +724,19 @@ def update_fabric_site(self, site, site_in_ccc): "isPubSubEnabled": pub_sub_enable } update_site_params.append(site_payload) - self.log("Requested payload for updating fabric site '{0}' is: {1}".format(site_name, str(site_payload)), "INFO") - - response = self.dnac._exec( - family="sda", - function='update_fabric_site', - op_modifies=True, - params={'payload': update_site_params} - ) - self.log("Received API response from 'update_fabric_site' for the site {0}: {1}".format(site_name, str(response)), "DEBUG") - response = response.get("response") + payload = {"payload": update_site_params} + task_name = "update_fabric_site" + task_id = self.get_taskid_post_api_call("sda", task_name, payload) - if not response: - self.status = "failed" - self.msg = "Unable to fetch the task Id for the updation of fabric site as the 'update_fabric_site' response is empty." - self.log(self.msg, "ERROR") + if not task_id: + self.msg = "Unable to retrieve the task_id for the task '{0}'.".format(task_name) + self.set_operation_result("failed", False, self.msg, "ERROR") return self - task_id = response.get("taskId") + success_msg = "Fabric site '{0}' updated successfully in the Cisco Catalyst Center".format(site_name) + self.get_task_status_from_tasks_by_id(task_id, task_name, success_msg) + self.update_site.append(site_name) - while True: - task_details = self.get_task_details(task_id) - if task_details.get("isError"): - self.status = "failed" - failure_reason = task_details.get("failureReason") - if failure_reason: - self.msg = "Unable to update the Fabric site '{0}' because of {1}.".format(site_name, failure_reason) - else: - self.msg = "Unable to update the Fabric site '{0}'.".format(site_name) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - break - elif task_details.get("endTime") and "workflow_id" in task_details.get("data"): - self.status = "success" - self.update_site.append(site_name) - self.log("Fabric site '{0}' updated successfully in the Cisco Catalyst Center".format(site_name), "INFO") - break - time.sleep(1) except Exception as e: self.status = "failed" self.msg = "An exception occured while updating the fabric site '{0}' in Cisco Catalyst Center: {1}".format(site_name, str(e)) @@ -820,54 +766,28 @@ def create_fabric_zone(self, zone): try: fabric_zone_payload = [] site_name = zone.get("site_name") - zone_payload = { "siteId": self.get_site_id(site_name), "authenticationProfileName": zone.get("authentication_profile"), } fabric_zone_payload.append(zone_payload) self.log("Requested payload for creating fabric zone '{0}' is: {1}".format(site_name, zone_payload), "INFO") + task_name = "add_fabric_site" + payload = {"payload": fabric_zone_payload} + task_id = self.get_taskid_post_api_call("sda", task_name, payload) - response = self.dnac._exec( - family="sda", - function='add_fabric_zone', - op_modifies=True, - params={'payload': fabric_zone_payload} - ) - self.log("Received API response from 'add_fabric_zone' for the site {0}: {1}".format(site_name, str(response)), "DEBUG") - response = response.get("response") - - if not response: - self.status = "failed" - self.msg = "Unable to fetch the task Id for the creation of fabric zone as the 'add_fabric_zone' response is empty." - self.log(self.msg, "ERROR") + if not task_id: + self.msg = "Unable to retrieve the task_id for the task '{0}'.".format(task_name) + self.set_operation_result("failed", False, self.msg, "ERROR") return self - task_id = response.get("taskId") - - while True: - task_details = self.get_task_details(task_id) + success_msg = "Fabric zone '{0}' created successfully in the Cisco Catalyst Center.".format(site_name) + self.get_task_status_from_tasks_by_id(task_id, task_name, success_msg) + self.create_zone.append(site_name) - if task_details.get("isError"): - self.status = "failed" - failure_reason = task_details.get("failureReason") - if failure_reason: - self.msg = "Unable to create the Fabric zone '{0}' because of {1}.".format(site_name, failure_reason) - else: - self.msg = "Unable to create the Fabric zone '{0}'.".format(site_name) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - break - elif task_details.get("endTime") and "workflow_id" in task_details.get("data"): - self.status = "success" - self.create_zone.append(site_name) - self.log("Fabric zone '{0}' created successfully in the Cisco Catalyst Center.".format(site_name), "INFO") - break - time.sleep(1) except Exception as e: - self.status = "failed" self.msg = "An exception occured while creating the fabric zone '{0}' in Cisco Catalyst Center: {1}".format(site_name, str(e)) - self.log(self.msg, "ERROR") + self.set_operation_result("failed", False, self.msg, "ERROR") return self @@ -1755,6 +1675,15 @@ def main(): supports_check_mode=False) ccc_fabric_sites = FabricSitesZones(module) + if ccc_fabric_sites.compare_dnac_versions(ccc_fabric_sites.get_ccc_version(), "2.3.7.6") < 0: + ccc_fabric_sites.msg = ( + "The specified version '{0}' does not support the SDA fabric devices feature. Supported versions start " + " from '2.3.7.6' onwards. Version '2.3.7.6' introduces APIs for creating, updating and deleting the " + "Fabric Sites/Zones and updating the Authentication profiles." + .format(ccc_fabric_sites.get_ccc_version()) + ) + ccc_fabric_sites.set_operation_result("failed", False, ccc_fabric_sites.msg, "ERROR").check_return_status() + state = ccc_fabric_sites.params.get("state") if state not in ccc_fabric_sites.supported_states: diff --git a/plugins/modules/sda_fabric_virtual_networks_workflow_manager.py b/plugins/modules/sda_fabric_virtual_networks_workflow_manager.py index 0a9cb076ef..c89b630c32 100644 --- a/plugins/modules/sda_fabric_virtual_networks_workflow_manager.py +++ b/plugins/modules/sda_fabric_virtual_networks_workflow_manager.py @@ -58,14 +58,14 @@ reserved VLANs 1002-1005, and 2046. If deploying on a fabric zone, this vlan_id must match the vlan_id of the corresponding layer2 virtual network on the fabric site. And updation of this field is not allowed. - type: str + type: int required: True fabric_site_locations: description: A list of fabric site locations where this VLAN is deployed, including site hierarchy and fabric type details. type: list elements: dict suboptions: - site_name: + site_name_hierarchy: description: This name uniquely identifies the site for operations such as creating/updating/deleting any fabric VLAN. This parameter is required, and updates to this field is not allowed. type: str @@ -108,7 +108,7 @@ type: list elements: dict suboptions: - site_name: + site_name_hierarchy: description: This name uniquely identifies the site for operations such as creating/updating/deleting any layer3 virtual network. type: str @@ -143,7 +143,7 @@ type: dict required: True suboptions: - site_name: + site_name_hierarchy: description: The hierarchical name of the site where the anycast gateway is deployed. type: str fabric_type: @@ -244,6 +244,8 @@ with VLAN be available for deletion. - For Layer 3 virtual networks, all Anycast Gateways associated with the given virtual network must be deleted first before the deletion operation for the virtual network is enabled. + - All newly created Layer3 Virtual Networks must either be assigned to one or more Fabric Sites, or they all must not be + assigned to any Fabric Sites. - SDK Method used are ccc_virtual_network.sda.get_site ccc_virtual_network.sda.get_fabric_sites @@ -3506,11 +3508,12 @@ def main(): # Initialize the Virtual Network object ccc_virtual_network = VirtualNetwork(module) - if ccc_virtual_network.get_ccc_version_as_integer() <= ccc_virtual_network.get_ccc_version_as_int_from_str("2.3.5.3"): + if ccc_virtual_network.compare_dnac_versions(ccc_virtual_network.get_ccc_version(), "2.3.7.6") < 0: ccc_virtual_network.msg = ( - "The provided Catalyst Center Version {ccc_version} does not support this workflow. " - "This workflow support starts from Catalyst Center Release {supported_version} onwards." - .format(ccc_version=ccc_virtual_network.get_ccc_version_as_string(), supported_version="2.3.5.3") + "The specified version '{0}' does not support the SDA fabric devices feature. Supported versions start " + " from '2.3.7.6' onwards. Version '2.3.7.6' introduces APIs for creating, updating and deleting the " + "Fabric VLAN, Virtual Networks and Anycast Gateways." + .format(ccc_virtual_network.get_ccc_version()) ) ccc_virtual_network.set_operation_result("failed", False, ccc_virtual_network.msg, "ERROR").check_return_status() 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: diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index da10da80e4..a01c25b930 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -432,7 +432,7 @@ class Site(DnacBase): def __init__(self, module): super().__init__(module) self.supported_states = ["merged", "deleted"] - self.created_site_list, self.updated_site_list, self.update_not_neeeded_sites = [], [], [] + self.created_site_list, self.updated_site_list, self.update_not_needed_sites = [], [], [] self.deleted_site_list, self.site_absent_list = [], [] self.keymap = {} @@ -1308,7 +1308,7 @@ def get_diff_merged(self, config): return {"error_message": error_message} if not self.site_requires_update(): - self.update_not_neeeded_sites.append(site_name) + self.update_not_needed_sites.append(site_name) self.log("Site - {0} does not need any update".format(site_name), "INFO") return self @@ -1598,7 +1598,7 @@ def get_diff_deleted(self, config): if self.compare_dnac_versions(self.get_ccc_version(), "2.3.5.3") <= 0: site_id = self.have.get("site_id") - api_response, response = self.get_device_ids_from_site(site_id) + api_response, response = self.get_device_ids_from_site(site_name, site_id) self.log( "Received API response from 'get_membership': {0}".format(str(api_response)), "DEBUG") @@ -1872,34 +1872,34 @@ def update_site_messages(self): """ if self.created_site_list and self.updated_site_list: self.result['changed'] = True - if self.update_not_neeeded_sites: + if self.update_not_needed_sites: msg = """Site(s) '{0}' created successfully as well as Site(s) '{1}' updated successully and the some site(s) '{2}' needs no update in Cisco Catalyst Center""" self.msg = msg.format(str(self.created_site_list), str( - self.updated_site_list), str(self.update_not_neeeded_sites)) + self.updated_site_list), str(self.update_not_needed_sites)) else: self.msg = """Site(s) '{0}' created successfully in Cisco Catalyst Center as well as Site(s) '{1}' updated successully in Cisco Catalyst Center""".format(str(self.created_site_list), str(self.updated_site_list)) elif self.created_site_list: self.result['changed'] = True - if self.update_not_neeeded_sites: + if self.update_not_needed_sites: self.msg = """Site(s) '{0}' created successfully and some site(s) '{1}' not needs any update in Cisco Catalyst - Center.""".format(str(self.created_site_list), str(self.update_not_neeeded_sites)) + Center.""".format(str(self.created_site_list), str(self.update_not_needed_sites)) else: self.msg = "Site(s) '{0}' created successfully in Cisco Catalyst Center.".format( str(self.created_site_list)) elif self.updated_site_list: self.result['changed'] = True - if self.update_not_neeeded_sites: + if self.update_not_needed_sites: self.msg = """Site(s) '{0}' updated successfully and some site(s) '{1}' not needs any update in Cisco Catalyst - Center.""".format(str(self.updated_site_list), str(self.update_not_neeeded_sites)) + Center.""".format(str(self.updated_site_list), str(self.update_not_needed_sites)) else: self.msg = "Site(s) '{0}' updated successfully in Cisco Catalyst Center.".format( str(self.updated_site_list)) - elif self.update_not_neeeded_sites: + elif self.update_not_needed_sites: self.result['changed'] = False self.msg = "Site(s) '{0}' not needs any update in Cisco Catalyst Center.".format( - str(self.update_not_neeeded_sites)) + str(self.update_not_needed_sites)) elif self.deleted_site_list and self.site_absent_list: self.result['changed'] = True self.msg = """Given site(s) '{0}' deleted successfully from Cisco Catalyst Center and unable to deleted some site(s) '{1}' as they diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 40f4903edf..182f79b4a3 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -206,9 +206,15 @@ If False then the given image will be un-tagged as golden. type: bool image_distribution_details: - description: Details for SWIM image distribution. Device on which the image needs to distributed - can be speciifed using any of the following parameters - deviceSerialNumber, - deviceIPAddress, deviceHostname or deviceMacAddress. + description: | + Parameters for specifying the target device(s) for SWIM image distribution. The device can be identified using one of the following options: + - device_serial_number + - device_ip_address + - device_hostname + - device_mac_address + - site_name (if specified, the image will be distributed to all devices within the site) + At least one of these parameters must be provided. If 'site_name' is provided, additional filters + such as 'device_role', 'device_family_name', and 'device_series_name' can be used to further narrow down the devices within the site. type: dict suboptions: device_role: @@ -255,9 +261,15 @@ description: Device MAC address where the image needs to be distributed type: str image_activation_details: - description: Details for SWIM image activation. Device on which the image needs to activated - can be speciifed using any of the following parameters - deviceSerialNumber, - deviceIPAddress, deviceHostname or deviceMacAddress. + description: | + Parameters for specifying the target device(s) for SWIM image activation. The device can be identified using one of the following options: + - device_serial_number + - device_ip_address + - device_hostname + - device_mac_address + - site_name (if specified, the image will be activated on all devices within the site) + At least one of these parameters must be provided. If 'site_name' is provided, additional filters + such as 'device_role', 'device_family_name', and 'device_series_name' can be used to further narrow down the devices within the site. type: dict suboptions: device_role: @@ -803,31 +815,46 @@ def get_device_id(self, params): self (object): An instance of a class used for interacting with Cisco Catalyst Center. params (dict): A dictionary containing parameters to filter devices. Returns: - str: The unique device ID corresponding to the filtered device. + str or None: The unique device ID corresponding to the filtered device, or None if an error occurs. + Raises: + AnsibleFailJson: If the device ID cannot be found in the response. Description: This function sends a request to Cisco Catalyst Center to retrieve a list of devices based on the provided filtering parameters. If a single matching device is found, it extracts and returns the device ID. If no device or multiple devices match the criteria, it raises an exception. """ - device_id = None - response = self.dnac._exec( - family="devices", - function='get_device_list', - op_modifies=True, - params=params, - ) - self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=True, + params=params, + ) + self.log("Received API response from 'get_device_list': {0}".format(str(response)), "DEBUG") - device_list = response.get("response") + device_list = response.get("response") - if (len(device_list) == 1): - device_id = device_list[0].get("id") - self.log("Device Id: {0}".format(str(device_id)), "INFO") - else: - self.msg = "Device with params: '{0}' not found in Cisco Catalyst Center so can't fetch the device id".format(str(params)) - self.log(self.msg, "WARNING") + if not device_list: + self.log("Device list is empty; no devices found for given parameters.", "WARNING") + raise ValueError("No devices found") + + if len(device_list) == 1: + device_id = device_list[0].get("id") + self.log("Successfully retrieved device ID: {0}".format(device_id), "INFO") + return device_id - return device_id + self.log("Multiple devices found for parameters: {0}".format(params), "ERROR") + raise ValueError("Multiple devices found") + + except ValueError as ve: + msg = "Error: {0}. Unable to fetch unique device ID with parameters: {1}".format(str(ve), params) + self.log(msg, "ERROR") + return None + + except Exception as e: + msg = "An unexpected error occurred while retrieving device ID: {0}".format(str(e)) + self.log(msg, "ERROR") + return None def get_device_uuids(self, site_name, device_family, device_role, device_series_name=None): """ @@ -1138,16 +1165,33 @@ def get_have(self): self.log("Image details required for distribution have not been provided", "ERROR") self.module.fail_json(msg="Image details required for distribution have not been provided", response=[]) - device_params = dict( - hostname=distribution_details.get("device_hostname"), - serialNumber=distribution_details.get("device_serial_number"), - managementIpAddress=distribution_details.get("device_ip_address"), - macAddress=distribution_details.get("device_mac_address"), - ) - device_id = self.get_device_id(device_params) + device_params = { + "hostname": distribution_details.get("device_hostname"), + "serialNumber": distribution_details.get("device_serial_number"), + "managementIpAddress": distribution_details.get("device_ip_address"), + "macAddress": distribution_details.get("device_mac_address") + } + + if any(device_params.values()): + device_id = self.get_device_id(device_params) + + if device_id is None: + params_list = [] + for key, value in device_params.items(): + if value: + formatted_param = "{0}: {1}".format(key, value) + params_list.append(formatted_param) + + params_message = ", ".join(params_list) + self.status = "failed" + self.msg = "The device with the following parameter(s): {0} could not be found in the Cisco Catalyst Center.".format(params_message) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + self.check_return_status() - if device_id is not None: - have["distribution_device_id"] = device_id + else: + self.log("Device with ID {0} found and added to distribution details.".format(device_id), "DEBUG") + have["distribution_device_id"] = device_id self.have.update(have) @@ -1174,18 +1218,41 @@ def get_have(self): have["site_id"] = site_id self.log("The site '{0}' exists and has the site ID '{1}'".format(site_name, str(site_id)), "INFO") - device_params = dict( - hostname=activation_details.get("device_hostname"), - serialNumber=activation_details.get("device_serial_number"), - managementIpAddress=activation_details.get("device_ip_address"), - macAddress=activation_details.get("device_mac_address"), - ) - device_id = self.get_device_id(device_params) + device_params = { + "hostname": activation_details.get("device_hostname"), + "serialNumber": activation_details.get("device_serial_number"), + "managementIpAddress": activation_details.get("device_ip_address"), + "macAddress": activation_details.get("device_mac_address") + } + + # Check if any device parameters are provided + if any(device_params.values()): + device_id = self.get_device_id(device_params) + + if device_id is None: + desired_keys = {"hostname", "serialNumber", "managementIpAddress", "macAddress"} + params_list = [] + + # Format only the parameters that are present + for key, value in device_params.items(): + if value and key in desired_keys: + formatted_param = "{0}: {1}".format(key, value) + params_list.append(formatted_param) + + params_message = ", ".join(params_list) + self.status = "failed" + self.msg = "The device with the following parameter(s): {0} could not be found in the Cisco Catalyst Center.".format(params_message) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + self.check_return_status() + + else: + have["activation_device_id"] = device_id + self.log("Device with ID {0} found and added to activation details.".format(device_id), "DEBUG") - if device_id is not None: - have["activation_device_id"] = device_id self.have.update(have) - self.log("Current State (have): {0}".format(str(self.have)), "INFO") + + self.log("Current State (have): {0}".format(str(self.have)), "INFO") return self @@ -1673,8 +1740,13 @@ def get_diff_distribution(self): self.complete_successful_distribution = False self.partial_successful_distribution = False self.single_device_distribution = False + distribution_device_id = self.have.get("distribution_device_id") + device_ip = self.get_device_ip_from_id(distribution_device_id) + image_name = self.want.get("distribution_details").get("image_name") - if self.have.get("distribution_device_id"): + if distribution_device_id: + self.log("Starting image distribution for device IP {0} with ID {1}, targeting software version {2}.".format( + device_ip, distribution_device_id, image_name), "INFO") distribution_params = dict( payload=[dict( deviceUuid=self.have.get("distribution_device_id"), @@ -1703,15 +1775,19 @@ def get_diff_distribution(self): self.result['changed'] = True self.status = "success" self.single_device_distribution = True - self.result['msg'] = "Image with Id {0} Distributed Successfully".format(image_id) - self.result['response'] = self.msg + self.result['msg'] = "Image '{0}' (ID: {1}) has been successfullyyy distributed to the device with IP address {2}.".format( + image_name, image_id, device_ip) + self.result['response'] = self.result['msg'] + self.log(self.result['msg']) break if task_details.get("isError"): self.status = "failed" - self.msg = "Image with Id {0} Distribution Failed".format(image_id) - self.log(self.msg, "ERROR") + self.msg = "Failed to distribute image '{0}' (ID: {1}) to the device with IP address {2}.".format( + image_name, image_id, device_ip) + self.result['msg'] = self.msg self.result['response'] = task_details + self.log(self.result['msg']) break self.result['response'] = task_details if task_details else response @@ -1730,6 +1806,7 @@ def get_diff_distribution(self): distribution_task_dict = {} for device_uuid in device_uuid_list: + self.log("Starting distribution of image '{0}' to multiple devices.".format(image_name)) device_management_ip = self.get_device_ip_from_id(device_uuid) distribution_params = dict( payload=[dict( @@ -1767,7 +1844,7 @@ def get_diff_distribution(self): self.result['changed'] = True self.status = "success" self.partial_successful_distribution = False - self.msg = "Image with Id '{0}' Distributed and partially successfull".format(image_id) + self.msg = "Image {0} with Id '{1}' Distributed and partially successfull".format(image_name, image_id) self.log("For device(s) {0} image Distribution gets failed".format(str(device_ips_list)), "CRITICAL") self.result['msg'] = self.msg @@ -1799,8 +1876,13 @@ def get_diff_activation(self): self.complete_successful_activation = False self.partial_successful_activation = False self.single_device_activation = False + activation_device_id = self.have.get("activation_device_id") + device_ip = self.get_device_ip_from_id(activation_device_id) + image_name = self.want.get("activation_details").get("image_name") - if self.have.get("activation_device_id"): + if activation_device_id: + self.log("Starting image activation for device IP {0} with ID {1}, targeting software version {2}.".format( + device_ip, activation_device_id, image_name), "INFO") payload = [dict( activateLowerImageVersion=activation_details.get("activate_lower_image_version"), deviceUpgradeMode=activation_details.get("device_upgrade_mode"), @@ -1832,17 +1914,19 @@ def get_diff_activation(self): if not task_details.get("isError") and \ ("completed successfully" in task_details.get("progress")): self.result['changed'] = True - self.result['msg'] = "Image Activated successfully" - self.result['response'] = self.msg + self.result['msg'] = "Image '{0}' (ID: {1}) has been successfully activated on the device with IP address {2}.".format( + image_name, image_id, device_ip) + self.result['response'] = self.result['msg'] self.status = "success" self.single_device_activation = True break if task_details.get("isError"): - self.msg = "Activation for Image with Id '{0}' gets failed".format(image_id) self.status = "failed" + self.msg = "Activation of image '{0}' (ID: {1}) to the device with IP address {2} has failed.".format(image_name, image_id, device_ip) + self.result['msg'] = self.msg self.result['response'] = task_details - self.log(self.msg, "ERROR") + self.log(self.result['msg'], "ERROR") return self self.result['response'] = task_details if task_details else response @@ -1861,6 +1945,7 @@ def get_diff_activation(self): activation_task_dict = {} for device_uuid in device_uuid_list: + self.log("Starting activation of image '{0}' to multiple devices.".format(image_name)) device_management_ip = self.get_device_ip_from_id(device_uuid) payload = [dict( activateLowerImageVersion=activation_details.get("activate_lower_image_version"), diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index 5bc116a126..2f28205671 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -__author__ = ['Madhan Sankaranarayanan, Rishita Chowdhary, Akash Bhaskaran, Muthu Rakesh'] +__author__ = ['Madhan Sankaranarayanan, Rishita Chowdhary, Akash Bhaskaran, Muthu Rakesh, Abhishek Maheshwari'] DOCUMENTATION = r""" --- @@ -29,6 +29,7 @@ Rishita Chowdhary (@rishitachowdhary) Akash Bhaskaran (@akabhask) Muthu Rakesh (@MUTHU-RAKESH-27) + Abhishek Maheshwari (@abmahesh) options: config_verify: description: Set to True to verify the Cisco Catalyst Center after applying the playbook config. @@ -809,6 +810,79 @@ description: ProjectName path parameter. Project name to create template under the project. type: str + deploy_template: + description: To deploy the template to the devices based on either list of site provisionig details with further filtering + criteria like device family, device role, device tag or by providing the device specific details which includes device_ips, + device_hostnames, serial_numbers or mac_addresses. + type: dict + suboptions: + project_name: + description: Provide the name of project under which the template is available. + type: str + template_name: + description: Name of the template to be deployed. + type: str + force_push: + description: Boolean flag to indicate whether the template should be forcefully pushed to the devices, overriding any existing + configuration. + type: bool + is_composite: + description: Boolean flag indicating whether the template is composite, which means the template is built using multiple smaller + templates. + type: bool + template_parameters: + description: A list of parameter name-value pairs used for customizing the template with specific values for each device. + type: list + elements: dict + suboptions: + param_name: + description: Name of the parameter in the template that needs to be replaced with a specific value. + type: str + param_value: + description: Value assigned to the parameter for deployment to devices. + type: str + device_details: + description: Details specific to devices where the template will be deployed, including lists of device IPs, hostnames, + serial numbers, or MAC addresses. + type: list + elements: dict + suboptions: + device_ips: + description: A list of IP addresses of the devices where the template will be deployed. + type: list + elements: str + device_hostnames: + description: A list of hostnames of the devices where the template will be deployed. + type: list + elements: str + serial_numbers: + description: A list of serial numbers of the devices where the template will be deployed. + type: list + elements: str + mac_addresses: + description: A list of MAC addresses of the devices where the template will be deployed. + type: list + elements: str + site_provisioning_details: + description: Parameters related to site-based provisioning, allowing the deployment of templates to devices associated with specific sites, with + optional filtering by device family, role, or tag. + type: list + elements: dict + suboptions: + site_name: + description: Name of the site where the devices are associated for provisioning. + type: list + elements: str + device_family: + description: Family of the devices (e.g., switches, routers) used to filter devices for template deployment. + type: str + device_role: + description: Role of the devices (e.g., access, core, edge) used to filter devices for template deployment. + type: str + device_tag: + description: Specific device tag used to filter devices for template deployment. + type: str + requirements: - dnacentersdk >= 2.7.2 @@ -953,6 +1027,81 @@ project_name: string template_file: string +- name: Deploy the given template to the devices based on site specific details and other filtering mode + cisco.dnac.template_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: merged + config_verify: True + config: + deploy_template: + project_name: "Sample_Project" + template_name: "Sample Template" + force_push: true + template_parameters: + - param_name: "vlan_id" + param_value: "1431" + - param_name: "vlan_name" + param_value: "testvlan31" + site_provisioning_details: + - site_name: "Global/Bangalore/Building14/Floor1" + device_family: "Switches and Hubs" + +- name: Deploy the given template to the devices based on device specific details + cisco.dnac.template_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: merged + config_verify: True + config: + deploy_template: + project_name: "Sample_Project" + template_name: "Sample Template" + force_push: true + template_parameters: + - param_name: "vlan_id" + param_value: "1431" + - param_name: "vlan_name" + param_value: "testvlan31" + device_details: + - device_ips: ["10.1.2.1", "10.2.3.4"] + +- name: Delete the given project or template from the Cisco Catalyst Center + cisco.dnac.template_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: deleted + config_verify: True + config: + configuration_templates: + project_name: "Sample_Project" + template_name: "Sample Template" + language: "velocity" + software_type: "IOS-XE" + device_types: + - product_family: "Switches and Hubs" + """ RETURN = r""" @@ -1027,6 +1176,7 @@ import copy import json +import time from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -1046,6 +1196,7 @@ def __init__(self, module): self.supported_states = ["merged", "deleted"] self.accepted_languages = ["JINJA", "VELOCITY"] self.export_template = [] + self.max_timeout = self.params.get('dnac_api_task_timeout') self.result['response'] = [ {"configurationTemplate": {"response": {}, "msg": {}}}, {"export": {"response": {}}}, @@ -1106,6 +1257,35 @@ def validate_input(self): 'template_name': {'type': 'str'}, 'version': {'type': 'str'} }, + 'deploy_template': { + 'type': 'dict', + 'project_name': {'type': 'str'}, + 'template_name': {'type': 'str'}, + 'force_push': {'type': 'bool'}, + 'is_composite': {'type': 'bool'}, + 'template_parameters': { + 'type': 'list', + 'elements': 'dict', + 'param_name': {'type': 'str'}, + 'param_value': {'type': 'str'}, + }, + 'device_details': { + 'type': 'list', + 'elements': 'dict', + 'device_ips': {'type': 'list', 'elements': 'str'}, + 'device_hostnames': {'type': 'list', 'elements': 'str'}, + 'serial_numbers': {'type': 'list', 'elements': 'str'}, + 'mac_addresses': {'type': 'list', 'elements': 'str'}, + }, + 'site_provisioning_details': { + 'type': 'list', + 'elements': 'dict', + 'site_name': {'type': 'str'}, + 'device_family': {'type': 'str'}, + 'device_role': {'type': 'str'}, + 'device_tag': {'type': 'str'}, + } + }, 'export': { 'type': 'dict', 'project': {'type': 'list', 'elements': 'str'}, @@ -1120,11 +1300,13 @@ def validate_input(self): 'type': 'dict', 'project': { 'type': 'dict', + 'project_file': {'type': 'str'}, 'do_version': {'type': 'str', 'default': 'False'}, }, 'template': { 'type': 'dict', 'do_version': {'type': 'str', 'default': 'False'}, + 'template_file': {'type': 'str'}, 'payload': { 'type': 'list', 'elements': 'dict', @@ -1443,6 +1625,40 @@ def get_templates_details(self, name): self.log("Received API response from 'get_templates_details': {0}".format(items), "DEBUG") return result + def get_project_defined_template_details(self, project_name, template_name): + """ + Get the template details from the template name provided in the playbook. + Parameters: + project_name (str) - Name of the project under which templates are associated. + template_name (str) - Name of the template provided in the playbook. + Returns: + template_details (dict) - Template details for the given template name. + """ + + self.log("Starting to retrieve template details for project '{0}' and template '{1}'.".format(project_name, template_name), "INFO") + template_details = None + try: + items = self.dnac_apply['exec']( + family="configuration_templates", + function="get_templates_details", + op_modifies=True, + params={ + "project_name": project_name, + "name": template_name + } + ) + if items: + template_details = items + self.log("Received template details for '{0}': {1}".format(template_name, template_details), "DEBUG") + else: + self.log("No template details found for project '{0}' and template '{1}'.".format(project_name, template_name), "WARNING") + + self.log("Received API response from 'get_templates_details': {0}".format(template_details), "DEBUG") + except Exception as e: + self.log("Exception occurred while retrieving template details for '{0}': {1}".format(template_name, str(e)), "ERROR") + + return template_details + def get_containing_templates(self, containing_templates): """ Store tags from the playbook for template processing in Cisco Catalyst Center. @@ -1656,6 +1872,111 @@ def get_template(self, config): self.result['response'][0].get("configurationTemplate").update({"items": items}) return result + def get_uncommitted_template_id(self, project_name, template_name): + """ + Retrieves the ID of an uncommitted template from a specified project in the Cisco Catalyst Center. + + Args: + self (object): An instance of the class used for interacting with Cisco Catalyst Center. + project_name (str): The name of the project under which the template is located. + template_name (str): The name of the template whose uncommitted ID is to be retrieved. + Returns: + str or None: The template ID if found, otherwise `None` if the template is not available or uncommitted. + Description: + This function queries the Cisco Catalyst Center for uncommitted templates within a specified project. + It checks if the template list contains the specified `template_name` and if found, returns the associated + `templateId`. If the template is not found, the function logs a warning message and returns `None`. + The function is useful for identifying templates that are not yet committed, which can then be versioned + or deployed. If the template is unavailable, an appropriate log message is recorded and the function + exits early with `None`. + """ + self.log("Retrieving uncommitted template ID for project '{0}' and template " + "'{1}'.".format(project_name, template_name), "INFO" + ) + template_id = None + try: + template_list = self.dnac_apply['exec']( + family="configuration_templates", + function="gets_the_templates_available", + op_modifies=False, + params={ + "projectNames": project_name, + "un_committed": True + }, + ) + if not template_list: + msg = ( + "No uncommitted templates available under the project '{0}'. " + "Cannot commit or deploy the template '{1}' in device(s)." + ).format(project_name, template_name) + self.log(msg, "WARNING") + return template_id + + for template in template_list: + if template.get("name") == template_name: + template_id = template.get("templateId") + self.log("Found uncommitted template '{0}' with ID: '{1}'.".format(template_name, template_id), "INFO") + return template_id + self.log("Template '{0}' not found in the uncommitted templates for project '{1}'.".format(template_name, project_name), "WARNING") + except Exception as e: + error_msg = ( + "Exception occurred while retrieving uncommitted template ID for project '{0}' and " + "template '{1}': {2}." + ).format(project_name, template_name, str(e)) + self.log(error_msg, "ERROR") + self.msg = error_msg + + return template_id + + def versioned_given_template(self, project_name, template_name, template_id): + """ + Versions (commits) a specified template in the Cisco Catalyst Center. + + Args: + self (object): An instance of the class used for interacting with Cisco Catalyst Center. + project_name (str): The name of the project under which the template resides. + template_name (str): The name of the template to be versioned. + template_id (str): The unique identifier of the template to be versioned. + Returns: + self (object): The instance of the class itself, with the operation result (success/failure) set accordingly. + Description: + This function handles the process of versioning or committing a template in the Cisco Catalyst Center. + It constructs a request payload with versioning comments and template ID, and then calls the API to + initiate the versioning task. + The function returns the class instance for further chaining of operations. + """ + + self.log("Starting the versioning process for template '{0}' in project '{1}'.".format(template_name, project_name), "INFO") + try: + comments = ( + "Given template '{0}' under the project '{1}' versioned successfully." + ).format(template_name, project_name) + + version_params = { + "comments": comments, + "templateId": template_id + } + self.log("Preparing to version template with parameters: {0}".format(version_params), "DEBUG") + task_name = "version_template" + task_id = self.get_taskid_post_api_call("configuration_templates", task_name, version_params) + + if not task_id: + self.msg = "Unable to retrieve the task_id for the task '{0}'.".format(task_name) + self.set_operation_result("failed", False, self.msg, "ERROR") + return self + + success_msg = "Given template '{0}' versioned/committed successfully in the Cisco Catalyst Center.".format(template_name) + self.get_task_status_from_tasks_by_id(task_id, task_name, success_msg) + + except Exception as e: + self.msg = ( + "An exception occured while versioning the template '{0}' in the Cisco Catalyst " + "Center: {1}" + ).format(template_name, str(e)) + self.set_operation_result("failed", False, self.msg, "ERROR") + + return self + def get_have_project(self, config): """ Get the current project related information from Cisco Catalyst Center. @@ -1769,6 +2090,7 @@ def get_have(self, config): Returns: self """ + have = {} configuration_templates = config.get("configuration_templates") if configuration_templates: if not configuration_templates.get("project_name"): @@ -1779,6 +2101,25 @@ def get_have(self, config): if template_available: self.get_have_template(config, template_available) + deploy_temp_details = config.get("deploy_template") + if deploy_temp_details: + template_name = deploy_temp_details.get("template_name") + project_name = deploy_temp_details.get("project_name") + self.log("Fetching template details for '{0}' under project '{1}'.".format(template_name, project_name), "INFO") + temp_details = self.get_project_defined_template_details(project_name, template_name).get("response") + + if temp_details: + self.log("Given template '{0}' is already committed in the Catalyst Center.".format(template_name), "INFO") + have["temp_id"] = temp_details[0].get("id") + + self.log("Successfully collected the details for the template '{0}' from the " + "Cisco Catalyst Center.".format(template_name), "INFO" + ) + else: + self.log("No details found for template '{0}' under project '{1}'.".format(template_name, project_name), "WARNING") + + self.have = have + self.msg = "Successfully collected all project and template \ parameters from Cisco Catalyst Center for comparison" self.status = "success" @@ -1830,6 +2171,42 @@ def get_want(self, config): want["project_params"] = project_params want["comments"] = version_comments + deploy_temp_details = config.get("deploy_template") + if deploy_temp_details: + project_name = deploy_temp_details.get("project_name") + if not project_name: + self.msg = ( + "To Deploy the template in the devices, parameter 'project_name' " + "must be given in the playboook." + ) + self.set_operation_result("failed", False, self.msg, "ERROR") + return self + + self.log("Project name '{0}' found in the playbook.".format(project_name), "INFO") + template_name = deploy_temp_details.get("template_name") + if not template_name: + self.msg = ( + "To Deploy the template in the devices, parameter 'template_name' " + "must be given in the playboook." + ) + self.set_operation_result("failed", False, self.msg, "ERROR") + return self + + self.log("Template name '{0}' found in the playbook.".format(template_name), "INFO") + device_details = deploy_temp_details.get("device_details") + site_provisioning_details = deploy_temp_details.get("site_provisioning_details") + + if not (device_details or site_provisioning_details): + self.msg = ( + "Either give the parameter 'device_details' or 'site_provisioning_details' " + "in the playbook to fetch the device ids and proceed for the deployment of template {0}." + ).format(template_name) + self.set_operation_result("failed", False, self.msg, "ERROR") + return self + + self.log("Proceeding with deployment details for template '{0}'.".format(template_name), "INFO") + want["deploy_tempate"] = deploy_temp_details + self.want = want self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" @@ -2184,7 +2561,8 @@ def update_configuration_templates(self, configuration_templates): else: self.msg = str(task_details.get("progress")) self.status = "failed" - return self + + return self def handle_export(self, export): """ @@ -2397,11 +2775,432 @@ def handle_import(self, _import): return self + def filter_devices_with_family_role(self, site_assign_device_ids, device_family=None, device_role=None): + """ + Filters devices based on their family and role from a list of site-assigned device IDs. + + Args: + self (object): An instance of the class interacting with Cisco Catalyst Center. + site_assign_device_ids (list): A list of device IDs (strings) assigned to a site that need to be filtered. + device_family (str, optional): The family of devices to filter by (e.g., 'Switches and Hubs'). If None, + this filter is not applied. Defaults to None. + device_role (str, optional): The role of the devices to filter by (e.g., 'ACCESS', 'CORE'). If None, + this filter is not applied. Defaults to None. + Returns: + list (str): A list of filtered device IDs (strings) that belong to the specified device family and role. + If no matching devices are found, the list will be empty. + Description: + This function filters a list of device IDs based on the specified `device_family` and `device_role` by querying + the Cisco Catalyst Center API. It iterates over each device ID, checking if the device belongs to the specified + family and has the desired role. Devices that match the criteria are added to the `filtered_device_list`. + If a device does not match the criteria or no response is received from the API, the function logs an + informational message and skips that device. In the event of an error during the API call, it logs the error + message and continues processing the remaining devices. + The function returns the list of devices that meet the filtering criteria. + """ + + filtered_device_list = [] + self.log("Filtering devices from the provided site-assigned device IDs: {0}, device_family='{1}', " + "and device_role='{2}'".format(site_assign_device_ids, device_family, device_role), "DEBUG" + ) + + for device_id in site_assign_device_ids: + try: + self.log("Processing device ID: {0}".format(device_id), "DEBUG") + response = self.dnac._exec( + family="devices", + function='get_device_list', + op_modifies=True, + params={ + "family": device_family, + "id": device_id, + "role": device_role + } + ) + if response and "response" in response: + response_data = response.get("response") + else: + self.log("No valid response for device with ID '{0}'.".format(device_id), "INFO") + continue + + if not response_data: + self.log( + "Device with ID '{0}' does not match family '{1}' or role '{2}'.".format(device_id, device_family, device_role), + "INFO" + ) + continue + + self.log("Device with ID '{0}' matches the criteria.".format(device_id), "DEBUG") + filtered_device_list.append(device_id) + + except Exception as e: + error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "CRITICAL") + continue + self.log("Completed filtering. Filtered devices: {0}".format(filtered_device_list), "DEBUG") + + return filtered_device_list + + def get_latest_template_version_id(self, template_id, template_name): + """ + Fetches the latest version ID of a specified template from the Cisco Catalyst Center. + + Args: + self (object): An instance of the class interacting with Cisco Catalyst Center. + template_id (str): The unique identifier of the template to retrieve its versions. + template_name (str): The name of the template for logging and reference purposes. + Returns: + str: The ID of the latest version of the template if available; otherwise, returns None. + Description: + This method calls the Cisco Catalyst Center API to fetch all versions of the specified template. + It selects the version with the most recent timestamp and retrieves its version ID. + If no versions are available or an error occurs during the API call, appropriate logs are generated. + """ + version_temp_id = None + self.log( + "Fetching the latest version ID for template '{0}' using template_id '{1}'.".format( + template_name, template_id), "DEBUG" + ) + + try: + response = self.dnac._exec( + family="configuration_templates", + function='get_template_versions', + op_modifies=True, + params={ + "template_id": template_id, + } + ) + + if not response or not isinstance(response, list) or not response[0].get("versionsInfo"): + self.log( + "No version information found for template '{0}' in Cisco Catalyst Center.".format(template_name), "INFO" + ) + return version_temp_id + + self.log( + "Successfully retrieved version information for template '{0}'.".format(template_name), "DEBUG" + ) + versions_info = response[0].get("versionsInfo") + self.log( + "Processing version details for template '{0}': {1}".format(template_name, str(versions_info)), "DEBUG" + ) + latest_version = max(versions_info, key=lambda x: x["versionTime"]) + version_temp_id = latest_version.get("id") + self.log( + "Identified the latest version for template '{0}'. Version ID: {1}".format( + template_name, version_temp_id), "DEBUG" + ) + + except Exception as e: + error_message = "Error while getting the latest version id for the template '{0}': '{1}'".format(template_name, str(e)) + self.log(error_message, "CRITICAL") + self.log( + "Returning latest version ID '{0}' for template '{1}'.".format(version_temp_id, template_name), "DEBUG" + ) + + return version_temp_id + + def create_payload_for_template_deploy(self, deploy_temp_details, device_ids): + """ + Creates a payload for deploying a template to specified devices in the Cisco Catalyst Center. + + Args: + self (object): An instance of the class interacting with Cisco Catalyst Center. + deploy_temp_details (dict): A dictionary containing details about the template to be deployed. + device_ids (list): A list of device UUIDs to which the template should be deployed. + Returns: + dict: A dictionary representing the payload required to deploy the template. + Description: + This function generates the necessary payload for deploying a template to devices in the Cisco Catalyst Center. + It first checks if the given template is already committed. If not, it fetches its uncommitted version, commits it, + and uses its template ID for deployment. The payload includes information about target devices and their respective + template parameters. + The function logs appropriate messages during the process, including if a template is already committed, if + parameters are updated, and when the payload is successfully collected. + """ + + project_name = deploy_temp_details.get("project_name") + template_name = deploy_temp_details.get("template_name") + self.log( + "Starting to create deployment payload for template '{0}' in project '{1}'." + .format(template_name, project_name), "DEBUG" + ) + # Check if the template is available but not yet committed + if self.have.get("temp_id"): + self.log( + "Template '{0}' is already committed in Cisco Catalyst Center. Using the committed template ID." + .format(template_name), "INFO" + ) + template_id = self.have.get("temp_id") + else: + self.log( + "Fetching uncommitted template ID for template '{0}' in project '{1}'.".format(template_name, project_name), + "DEBUG" + ) + template_id = self.get_uncommitted_template_id(project_name, template_name) + + if not template_id: + self.msg = ( + "Unable to fetch the details for the template '{0}' from the Cisco " + "Catalyst Center." + ).format(template_name) + self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status() + + self.log( + "Template '{0}' is available but not committed yet. Committing template...".format(template_name), + "INFO" + ) + + # Commit or versioned the given template in the Catalyst Center + self.versioned_given_template(project_name, template_name, template_id).check_return_status() + + deploy_payload = { + "forcePushTemplate": deploy_temp_details.get("force_push", False), + "isComposite": deploy_temp_details.get("is_composite", False), + "templateId": template_id, + } + self.log( + "Handling template parameters for the deployment of template '{0}'.".format(template_name), + "DEBUG" + ) + target_info_list = [] + template_dict = {} + template_parameters = deploy_temp_details.get("template_parameters") + if not template_parameters: + self.msg = ( + "It appears that no template parameters were provided in the playbook. Unfortunately, this " + "means we cannot proceed with deploying template '{0}' to the devices." + ).format(template_name) + self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status() + + for param in template_parameters: + name = param["param_name"] + value = param["param_value"] + self.log("Update the template placeholder for the name '{0}' with value {1}".format(name, value), "DEBUG") + template_dict[name] = value + + # Get the latest version template ID + version_template_id = self.get_latest_template_version_id(template_id, template_name) + if not version_template_id: + self.log("No versioning found for the template: {0}".format(template_name), "INFO") + version_template_id = template_id + + self.log("Preparing to deploy template '{0}' to the following device IDs: '{1}'".format(template_name, device_ids), "DEBUG") + for device_id in device_ids: + self.log("Adding device '{0}' to the deployment payload.".format(device_id), "DEBUG") + target_device_dict = { + "id": device_id, + "type": "MANAGED_DEVICE_UUID", + "versionedTemplateId": version_template_id, + "params": template_dict, + } + target_info_list.append(target_device_dict) + del target_device_dict + + deploy_payload["targetInfo"] = target_info_list + self.log("Successfully generated deployment payload for template '{0}'.".format(template_name), "INFO") + + return deploy_payload + + def deploy_template_to_devices(self, deploy_temp_payload, template_name, device_ips): + """ + Deploys a specified template to devices associated with a site in the Cisco Catalyst Center. + + Args: + self (object): An instance of the class used for interacting with Cisco Catalyst Center. + deploy_temp_payload (dict): The payload containing the details required to deploy the template. + This includes the template ID, device details, and template parameters. + template_name (str): The name of the template to be deployed. + device_ips (list): The management ip address of the devices to which template will be deployed. + Returns: + self (object): The instance of the class itself, with the operation result (success or failure) + set accordingly. + Description: + This function handles the deployment of a template to a set of devices managed in the Cisco Catalyst Center. + It sends a POST request with the deployment payload and retrieves the task ID associated with the deployment task. + It then monitors the status of the task using the task ID and logs the result. + If the task ID is not retrieved or an exception occurs during deployment, the function logs an error message, + sets the operation result to "failed," and returns the instance. + The success message indicates that the template has been successfully deployed to all the devices in the specified + site, while any exceptions are caught and logged with appropriate details. + """ + + try: + self.log("Deploying the given template {0} to the device(s) {1}.".format(template_name, device_ips)) + payload = {"payload": deploy_temp_payload} + task_name = "deploy_template_v2" + task_id = self.get_taskid_post_api_call("configuration_templates", task_name, payload) + + if not task_id: + self.msg = "Unable to retrieve the task_id for the task '{0}'.".format(task_name) + self.set_operation_result("failed", False, self.msg, "ERROR") + return self + + loop_start_time = time.time() + sleep_duration = self.params.get('dnac_task_poll_interval') + self.log("Starting task monitoring for '{0}' with task ID '{1}'.".format(task_name, task_id), "DEBUG") + + while True: + task_details = self.get_task_details_by_id(task_id) + if not task_details: + 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") + return self + + # 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" + ) + return self + + progress = task_details.get("progress") + self.log("Task ID '{0}' details for the API '{1}': {2}".format(task_id, task_name, progress), "DEBUG") + + if "not deploying" in progress: + self.log("Deployment of the template {0} gets failed because of: {1}".format(template_name, progress), "WARNING") + self.msg = progress + self.set_operation_result("failed", False, self.msg, "ERROR") + return self + + if "ApplicableTargets" in progress: + self.msg = ( + "Given template '{0}' deployed successfully to all the device(s) '{1}' " + " in the Cisco Catalyst Center." + ).format(template_name, device_ips) + self.set_operation_result("success", True, self.msg, "INFO") + return self + + self.log("Waiting for {0} seconds before checking the task status again.".format(sleep_duration), "DEBUG") + time.sleep(sleep_duration) + + except Exception as e: + self.msg = ( + "An exception occured while deploying the template '{0}' to the device(s) {1} " + " in the Cisco Catalyst Center: {2}." + ).format(template_name, device_ips, str(e)) + self.set_operation_result("failed", False, self.msg, "ERROR") + + return self + + def get_device_ips_from_config_priority(self, device_details): + """ + Retrieve device IPs based on the configuration. + Parameters: + - self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. + Returns: + list: A list containing device IPs. + Description: + This method retrieves device IPs based on the priority order specified in the configuration. + It first checks if device IPs are available. If not, it checks hostnames, serial numbers, + and MAC addresses in order and retrieves IPs based on availability. + If none of the information is available, an empty list is returned. + """ + # Retrieve device IPs from the configuration + self.log("Retrieving device IPs based on the configuration priority with details: {0}".format(device_details), "INFO") + try: + device_ips = device_details.get("device_ips") + + if device_ips: + self.log("Found device IPs: {0}".format(device_ips), "INFO") + return device_ips + + # If device IPs are not available, check hostnames + device_hostnames = device_details.get("device_hostnames") + if device_hostnames: + self.log("No device IPs found. Checking hostnames: {0}".format(device_hostnames), "INFO") + device_ip_dict = self.get_device_ips_from_hostnames(device_hostnames) + return self.get_list_from_dict_values(device_ip_dict) + + # If hostnames are not available, check serial numbers + device_serial_numbers = device_details.get("serial_numbers") + if device_serial_numbers: + self.log("No device IPs or hostnames found. Checking serial numbers: {0}".format(device_serial_numbers), "INFO") + device_ip_dict = self.get_device_ips_from_serial_numbers(device_serial_numbers) + return self.get_list_from_dict_values(device_ip_dict) + + # If serial numbers are not available, check MAC addresses + device_mac_addresses = device_details.get("mac_addresses") + if device_mac_addresses: + self.log("No device IPs, hostnames, or serial numbers found. Checking MAC addresses: {0}".format(device_mac_addresses), "INFO") + device_ip_dict = self.get_device_ips_from_mac_addresses(device_mac_addresses) + return self.get_list_from_dict_values(device_ip_dict) + + # If no information is available, return an empty list + self.log("No device information available to retrieve IPs.", "WARNING") + return [] + + except Exception as e: + self.log("No device information available to retrieve IPs.", "WARNING") + return [] + + def get_device_ids_from_tag(self, tag_name, tag_id): + """ + Retrieves the device IDs associated with a specific tag from the Cisco Catalyst Center. + + Args: + self (object): An instance of the class used for interacting with Cisco Catalyst Center. + tag_name (str): The name of the tag, used for logging purposes. + tag_id (str): The unique identifier of the tag from which to retrieve associated device IDs. + Returns: + list (str): A list of device IDs (strings) associated with the specified tag. If no devices are found or + an error occurs, the function returns an empty list. + Description: + This function queries the Cisco Catalyst Center API to retrieve a list of devices associated with a given tag. + It calls the `get_tag_members_by_id` function using the tag's ID, specifying that the tag members should be of + type "networkdevice". If the API response contains device data, the function extracts and returns the device IDs. + The function logs whether the tag has associated devices and details about the API response. In the event of an + exception, it logs an error message, sets the operation result to "failed," and returns an empty list. + """ + + device_ids = [] + self.log("Fetching device IDs associated with the tag '{0}' (ID: {1}).".format(tag_name, tag_id), "INFO") + + try: + response = self.dnac._exec( + family="tag", + function='get_tag_members_by_id', + op_modifies=False, + params={ + "id": tag_id, + "member_type": "networkdevice", + } + ) + if response and "response" in response: + response_data = response.get("response") + else: + self.log("No valid response for device with tag ID '{0}'.".format(tag_id), "INFO") + return device_ids + + if not response_data: + self.log("No device(s) are associated with the tag '{0}'.".format(tag_name), "WARNING") + return device_ids + + self.log("Received API response from 'get_tag_members_by_id' for the tag {0}: {1}".format(tag_name, response_data), "DEBUG") + for tag in response_data: + device_id = tag.get("id") + self.log("Device ID '{0}' found for tag '{1}'.".format(device_id, tag_name), "DEBUG") + device_ids.append(device_id) + + except Exception as e: + self.msg = ( + "Exception occurred while fetching tag id for the tag '{0} 'from " + "Cisco Catalyst Center: {1}" + ).format(tag_name, str(e)) + self.set_operation_result("failed", False, self.msg, "INFO").check_return_status() + + return device_ids + def get_diff_merged(self, config): """ Update/Create templates and projects in CCC with fields provided in Cisco Catalyst Center. Export the tempaltes and projects. Import the templates and projects. + Deploy the template to the devices based on device specific details or by fetching the device + details from site using other filtering parameters like device tag, device family, device role. Check using check_return_status(). Parameters: @@ -2413,24 +3212,143 @@ def get_diff_merged(self, config): configuration_templates = config.get("configuration_templates") if configuration_templates: - self.update_configuration_templates(configuration_templates) - if self.status == "failed": - return self + self.update_configuration_templates(configuration_templates).check_return_status() + + _import = config.get("import") + if _import: + self.handle_import(_import).check_return_status() export = config.get("export") if export: - self.handle_export(export) - if self.status == "failed": - return self + self.log("Found export configuration: {0}".format(export), "DEBUG") + self.handle_export(export).check_return_status() + + deploy_temp_details = config.get("deploy_template") + if deploy_temp_details: + template_name = deploy_temp_details.get("template_name") + device_details = deploy_temp_details.get("device_details") + site_specific_details = deploy_temp_details.get("site_provisioning_details") + self.log("Deploy template details found for template '{0}'".format(template_name), "DEBUG") + self.log("Device specific details: {0}".format(device_details), "DEBUG") + self.log("Site associated provisioning details: {0}".format(site_specific_details), "DEBUG") + + if device_details: + self.log("Attempting to retrieve device IPs based on priority from device specific details.", "DEBUG") + device_ips = self.get_device_ips_from_config_priority(device_details) + if not device_ips: + self.msg = ( + "No matching device management IP addresses found for the " + "deployment of template '{0}'." + ).format(template_name) + self.set_operation_result("failed", False, self.msg, "ERROR") + return self - _import = config.get("import") - if _import: - self.handle_import(_import) - if self.status == "failed": + self.log("Successfully retrieved device IPs for template '{0}': '{1}'".format(template_name, device_ips), "INFO") + device_id_dict = self.get_device_ids_from_device_ips(device_ips) + device_ids = self.get_list_from_dict_values(device_id_dict) + + device_missing_msg = ( + "There are no device id found for the device(s) '{0}' in the " + "Cisco Catalyst Center so cannot deploy the given template '{1}'." + ).format(device_ips, template_name) + elif site_specific_details: + device_ids, site_name_list = [], [] + + for site in site_specific_details: + site_name = site.get("site_name") + site_exists, site_id = self.get_site_id(site_name) + self.log("Checking if the site '{0}' exists in Cisco Catalyst Center.".format(site_name), "DEBUG") + if not site_exists: + self.msg = ( + "To Deploy the template in the devices, given site '{0}' must be " + "present in the Cisco Catalyst Center and it's not there currently." + ).format(site_name) + self.set_operation_result("failed", False, self.msg, "ERROR") + return self + + self.log("Retrieving devices associated with site ID '{0}' for site '{1}'.".format(site_id, site_name), "DEBUG") + site_response, site_assign_device_ids = self.get_device_ids_from_site(site_name, site_id) + site_name_list.append(site_name) + + if not site_assign_device_ids: + device_missing_msg = ( + "There is no device currently associated with the site '{0}' in the " + "Cisco Catalyst Center so cannot deploy the given template '{1}'." + ).format(site_name, template_name) + self.msg = device_missing_msg + self.log(device_missing_msg, "WARNING") + continue + + device_family = site.get("device_family") + device_role = site.get("device_role") + + # Filter devices based on the device family or device role + if device_family or device_role: + self.log( + "Filtering devices based on the device family '{0}' or role '{1}' for the site '{2}'.".format( + device_family, device_role, site_name), "DEBUG" + ) + self.log("Filtering devices based on the given family/role for the site {0}.".format(site_name), "INFO") + site_assign_device_ids = self.filter_devices_with_family_role(site_assign_device_ids, device_family, device_role) + + # Filter devices based on the device tag given to the devices + tag_name = site.get("device_tag") + tag_device_ids = None + if tag_name: + self.log("Filtering out the devices based on the given device tag: '{0}'".format(tag_name), "INFO") + tag_id = self.get_network_device_tag_id(tag_name) + self.log("Successfully collected the tag id '{0}' for the tag '{1}'".format(tag_id, tag_name), "INFO") + # Get the device ids associated with the given tag for given site + tag_device_ids = self.get_device_ids_from_tag(tag_name, tag_id) + self.log("Successfully collected the device ids {0} associated with the tag {1}".format(tag_device_ids, tag_name), "INFO") + + self.log("Getting the device ids based on device assoicated with tag or site or both.", "DEBUG") + + if tag_device_ids and site_assign_device_ids: + self.log("Determining device IDs from site and tag criteria.", "DEBUG") + common_device_ids = list(set(tag_device_ids).intersection(set(site_assign_device_ids))) + device_ids.extend(common_device_ids) + elif site_assign_device_ids and not tag_device_ids: + self.log("Getting the device ids based on devices fetched from site.", "DEBUG") + device_ids.extend(site_assign_device_ids) + elif tag_device_ids and not site_assign_device_ids: + self.log("Getting the device ids based on devices fetched with the tag {0}.".format(tag_name), "DEBUG") + device_ids.extend(tag_device_ids) + else: + self.log( + "There is no matching device ids found for the deployment of template {0} " + "for the given site {1}".format(template_name, site_name), "WARNING" + ) + continue + + device_missing_msg = ( + "There is no device id found for the given site(s) '{0}' in the " + "Cisco Catalyst Center so cannot deploy the template '{1}'." + ).format(site_name_list, template_name) + else: + self.msg = ( + "Unable to provision the template '{0}' as device related details are " + "not given in the playboook. Please provide it either via the parameter " + "device_details or with site_provisioning_details." + ).format(self.msg) + self.set_operation_result("failed", False, self.msg, "INFO").check_return_status() + + if not device_ids: + self.msg = device_missing_msg + self.set_operation_result("failed", False, self.msg, "INFO") return self + device_ip_dict = self.get_device_ips_from_device_ids(device_ids) + device_ips = self.get_list_from_dict_values(device_ip_dict) + self.log("Successfully collect the device ips {0} for the device ids {1}.".format(device_ips, device_ids), "INFO") + deploy_temp_payload = self.create_payload_for_template_deploy(deploy_temp_details, device_ids) + self.log("Deployment payload created successfully for template '{0}'.".format(template_name), "INFO") + self.deploy_template_to_devices(deploy_temp_payload, template_name, device_ips).check_return_status() + self.log("Successfully deployed template '{0}'.".format(template_name), "INFO") + self.msg = "Successfully completed merged state execution" self.status = "success" + return self def delete_project_or_template(self, config, is_delete_project=False): @@ -2462,21 +3380,43 @@ def delete_project_or_template(self, config, is_delete_project=False): params=params_key, ) task_id = response.get("response").get("taskId") - if task_id: - task_details = self.get_task_details(task_id) - self.result['changed'] = True - self.result['response'][0].get("configurationTemplate")['msg'] = task_details.get('progress') - self.result['response'][0].get("configurationTemplate")['diff'] = config.get("configuration_templates") + sleep_duration = self.params.get('dnac_task_poll_interval') + if not task_id: + self.msg = "Unable to retrieve the task ID for the task '{0}'.".format(deletion_value) + self.set_operation_result("failed", False, self.msg, "ERROR") + return self - self.log("Task details for '{0}': {1}".format(deletion_value, task_details), "DEBUG") - self.result['response'][0].get("configurationTemplate")['response'] = task_details if task_details else response - if not self.result['response'][0].get("configurationTemplate")['msg']: - self.result['response'][0].get("configurationTemplate")['msg'] = "Error while deleting {name} : " - self.status = "failed" + while True: + task_details = self.get_task_details_by_id(task_id) + self.log("Printing task details: {0}".format(task_details), "DEBUG") + if not task_details: + self.msg = "Unable to delete {0} as task details is empty.".format(deletion_value) + self.set_operation_result("failed", False, self.msg, "ERROR") return self - self.msg = "Successfully deleted {0} ".format(name) - self.status = "success" + progress = task_details.get("progress") + self.log("Task details for the API {0}: {1}".format(deletion_value, progress), "DEBUG") + + if "deleted" in progress: + self.log("Successfully perform the operation of {0} for {1}".format(deletion_value, name), "INFO") + self.msg = "Successfully deleted {0} ".format(name) + self.set_operation_result("success", True, self.msg, "INFO") + break + + if task_details.get("isError"): + failure_reason = task_details.get("failureReason") + if failure_reason: + self.msg = ( + "Failed to perform the operation of {0} for {1} because of: {2}" + ).format(deletion_value, name, failure_reason) + else: + self.msg = "Failed to perform the operation of {0} for {1}.".format(deletion_value, name) + self.set_operation_result("failed", False, self.msg, "ERROR") + break + + self.log("Waiting for {0} seconds before checking the task status again.".format(sleep_duration), "DEBUG") + time.sleep(sleep_duration) + return self def get_diff_deleted(self, config): @@ -2525,6 +3465,15 @@ def get_diff_deleted(self, config): self.status = "failed" return self + deploy_temp_details = config.get("deploy_template") + if deploy_temp_details: + template_name = deploy_temp_details.get("template_name") + self.msg = ( + "Deleting or removing the device configuration using deployment of template is not supported " + "for the template {0} in the Cisco Catalyst Center." + ).format(template_name) + self.set_operation_result("failed", False, self.msg, "ERROR").check_return_status() + self.msg = "Successfully completed delete state execution" self.status = "success" return self diff --git a/plugins/modules/user_role_workflow_manager.py b/plugins/modules/user_role_workflow_manager.py index f5602cfcdd..6e1d0072e5 100644 --- a/plugins/modules/user_role_workflow_manager.py +++ b/plugins/modules/user_role_workflow_manager.py @@ -1038,32 +1038,50 @@ def validate_password(self, password, error_messages): Returns: None: This function does not return a value, but it may append an error message to `error_messages` if the password is invalid. Criteria: - - The password must be 8 to 20 characters long. + - The password must be 9 to 20 characters long. - The password must include characters from at least three of the following classes: lowercase letters, uppercase letters, digits, and special characters. """ - is_valid_password = False + meets_character_requirements = False password_criteria_message = ( - "Password must be 8 to 20 characters long and include characters from at least three of " - "the following classes: lowercase letters, uppercase letters, digits, and special characters." + "The password must be 9 to 20 characters long and include at least three of the following " + "character types: lowercase letters, uppercase letters, digits, and special characters. " + "Additionally, the password must not contain repetitive or sequential characters." ) self.log(password_criteria_message, "DEBUG") password_regexs = [ - re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?!.*[\W_]).{8,20}$'), - re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_])(?!.*\d).{8,20}$'), - re.compile(r'^(?=.*[a-z])(?=.*\d)(?=.*[\W_])(?!.*[A-Z]).{8,20}$'), - re.compile(r'^(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*[a-z]).{8,20}$'), - re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,20}$') + re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?!.*[\W_]).{9,20}$'), + re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_])(?!.*\d).{9,20}$'), + re.compile(r'^(?=.*[a-z])(?=.*\d)(?=.*[\W_])(?!.*[A-Z]).{9,20}$'), + re.compile(r'^(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*[a-z]).{9,20}$'), + re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{9,20}$') ] + password_sequence_repetitive_regex = re.compile( + r'^(?!.*(.)\1{3})' + r'(?!.*(?:012|123|234|345|456|567|678|789|' + r'abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|' + r'opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|' + r'ABC|BCD|CDE|DEF|EFG|FGH|GHI|HIJ|IJK|JKL|KLM|LMN|MNO|NOP|' + r'OPQ|PQR|QRS|RST|STU|TUV|UVW|VWX|WXY|XYZ)).*' + r'[a-zA-Z0-9!@#$%^&*()_+<>?]{9,20}$' + ) + self.log("Password meets character type and length requirements.", "INFO") for password_regex in password_regexs: if password_regex.match(password): - is_valid_password = True + meets_character_requirements = True break - if not is_valid_password: - self.log("Password validation failed: {0}".format(password_criteria_message), "DEBUG") + if not meets_character_requirements: + self.log("Password failed character type and length validation.", "ERROR") + error_messages.append(password_criteria_message) + + self.log("Checking that the password does not contain repetitive or sequential characters.", "DEBUG") + if re.match(password_sequence_repetitive_regex, password): + self.log("Password passed repetitive and sequential character checks.", "INFO") + else: + self.log("Password failed repetitive or sequential character validation.", "ERROR") error_messages.append(password_criteria_message) def validate_role_parameters(self, role_key, params_list, role_config, role_param_map, error_messages): 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