diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 737b3f90ff..7789103184 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -1107,16 +1107,11 @@ releases: - Changes in site_workflow_manager - Modifications due to documentation errors 6.25.1: - release_date: "2024-12-04" - changes: - release_summary: application of unapplied changes. - minor_changes: - - application of the changes made in pull request 207 - 6.25.2: release_date: "2024-12-05" changes: - release_summary: Alias implementation. + release_summary: application of unapplied changes and Alias implementation minor_changes: + - application of the changes made in pull request 207 - Bug fixes in accesspoint_workflow_manager module - Changes in sda_fabric_devices_workflow_manager module - Bug fixes in [sda_fabric_sites_zones_workflow_manager module diff --git a/galaxy.yml b/galaxy.yml index b48a97aefd..c08001f0ae 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.25.2 +version: 6.25.1 readme: README.md authors: - Rafael Campos diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index 72bcc39d02..227367c1ea 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -21,47 +21,47 @@ config_verify: true state: merged config: - - username: "{{item.username}}" - password: "{{item.password}}" - enable_password: "{{item.enable_password}}" - ip_address_list: "{{item.ip_address_list}}" - cli_transport: "{{item.cli_transport}}" - snmp_auth_passphrase: "{{item.snmp_auth_passphrase}}" - snmp_auth_protocol: "{{item.snmp_auth_protocol}}" - snmp_mode: "{{item.snmp_mode}}" - snmp_priv_passphrase: "{{item.snmp_priv_passphrase}}" - snmp_priv_protocol: "{{item.snmp_priv_protocol}}" - snmp_ro_community: "{{item.snmp_ro_community}}" - snmp_rw_community: "{{item.snmp_rw_community}}" - snmp_username: "{{item.snmp_username}}" - credential_update: "{{item.credential_update}}" - clean_config: "{{item.clean_config}}" - type: "{{item.type}}" - device_resync: "{{item.device_resync}}" - reboot_device: "{{item.reboot_device}}" - role: "{{item.role}}" - add_user_defined_field: - - name: Test123 - description: "Added first udf for testing" - value: "value123" - - name: Test321 - description: "Added second udf for testing" - value: "value321" - provision_wired_device: - - device_ip: "1.1.1.1" - site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" - resync_retry_count: 200 - resync_interval: 2 - - device_ip: "2.2.2.2" - site_name: "Global/USA/San Francisco/BGL_18/floor_test" - resync_retry_count: 200 - resync_retry_interval: 2 - update_interface_details: - description: "{{item.update_interface_details.description}}" - interface_name: "{{item.interface_name}}" - export_device_list: - password: "{{item.export_device_list.password}}" - - with_items: "{{ device_details }}" - tags: - - inventory_device + - username: "{{item.username}}" + password: "{{item.password}}" + enable_password: "{{item.enable_password}}" + ip_address_list: "{{item.ip_address_list}}" + cli_transport: "{{item.cli_transport}}" + snmp_auth_passphrase: "{{item.snmp_auth_passphrase}}" + snmp_auth_protocol: "{{item.snmp_auth_protocol}}" + snmp_mode: "{{item.snmp_mode}}" + snmp_priv_passphrase: "{{item.snmp_priv_passphrase}}" + snmp_priv_protocol: "{{item.snmp_priv_protocol}}" + snmp_ro_community: "{{item.snmp_ro_community}}" + snmp_rw_community: "{{item.snmp_rw_community}}" + snmp_username: "{{item.snmp_username}}" + credential_update: "{{item.credential_update}}" + clean_config: "{{item.clean_config}}" + type: "{{item.type}}" + device_resync: "{{item.device_resync}}" + reboot_device: "{{item.reboot_device}}" + role: "{{item.role}}" + add_user_defined_field: + - name: Test123 + description: "Added first udf for testing" + value: "value123" + - name: Test321 + description: "Added second udf for testing" + value: "value321" + provision_wired_device: + - device_ip: "1.1.1.1" + site_name: "Global/USA/San Francisco/BGL_18/floor_pnp" + resync_retry_count: 200 + resync_interval: 2 + - device_ip: "2.2.2.2" + site_name: "Global/USA/San Francisco/BGL_18/floor_test" + resync_retry_count: 200 + resync_retry_interval: 2 + update_interface_details: + description: "{{item.update_interface_details.description}}" + interface_name: "{{item.interface_name}}" + export_device_list: + password: "{{item.export_device_list.password}}" + + with_items: "{{ device_details }}" + tags: + - inventory_device diff --git a/playbooks/sda_fabric_devices_workflow_manager.yml b/playbooks/sda_fabric_devices_workflow_manager.yml index 3cc500b03f..7c9ef05e09 100644 --- a/playbooks/sda_fabric_devices_workflow_manager.yml +++ b/playbooks/sda_fabric_devices_workflow_manager.yml @@ -20,40 +20,40 @@ state: merged config_verify: true config: - - fabric_devices: - fabric_name: Global/USA/SAN-JOSE - device_config: - - device_ip: 10.0.0.1 - device_roles: [CONTROL_PLANE_NODE, EDGE_NODE] - borders_settings: - layer3_settings: - local_autonomous_system_number: 213 - is_default_exit: true - import_external_routes: true - border_priority: 1 - prepend_autonomous_system_count: 1 + - fabric_devices: + fabric_name: Global/USA/SAN-JOSE + device_config: + - device_ip: 10.0.0.1 + device_roles: [CONTROL_PLANE_NODE, EDGE_NODE] + borders_settings: + layer3_settings: + local_autonomous_system_number: 213 + is_default_exit: true + import_external_routes: true + border_priority: 1 + prepend_autonomous_system_count: 1 - layer3_handoff_ip_transit: - - transit_network_name: IP_TRANSIT_1 - interface_name: FortyGigabitEthernet1/1/1 - external_connectivity_ip_pool_name: reserved_pool_1 - virtual_network_name: L3VN1 - vlan_id: 333 - tcp_mss_adjustment: 510 + layer3_handoff_ip_transit: + - transit_network_name: IP_TRANSIT_1 + interface_name: FortyGigabitEthernet1/1/1 + external_connectivity_ip_pool_name: reserved_pool_1 + virtual_network_name: L3VN1 + vlan_id: 333 + tcp_mss_adjustment: 510 - layer3_handoff_sda_transit: - transit_network_name: SDA_PUB_SUB_TRANSIT - affinity_id_prime: 2 - affinity_id_decider: 2 - connected_to_internet: true - is_multicast_over_transit_enabled: true + layer3_handoff_sda_transit: + transit_network_name: SDA_PUB_SUB_TRANSIT + affinity_id_prime: 2 + affinity_id_decider: 2 + connected_to_internet: true + is_multicast_over_transit_enabled: true - layer2_handoff: - - interface_name: FortyGigabitEthernet1/1/1 - internal_vlan_id: 443 - external_vlan_id: 444 + layer2_handoff: + - interface_name: FortyGigabitEthernet1/1/1 + internal_vlan_id: 443 + external_vlan_id: 444 - - name: Delete the SDA fabric device and remove L2 and L3 handoff configurations + - name: Delete the SDA fabric device's L2 and L3 handoff configurations cisco.dnac.sda_fabric_devices_workflow_manager: dnac_host: "{{ dnac_host }}" dnac_port: "{{ dnac_port }}" @@ -66,23 +66,43 @@ dnac_log_level: DEBUG dnac_log_append: true dnac_log_file_path: "{{ dnac_log_file_path }}" - state: merged + state: deleted config_verify: true config: - - fabric_devices: - fabric_name: Global/USA/SAN-JOSE - device_config: - - device_ip: 10.0.0.1 - delete_fabric_device: true - borders_settings: - layer3_handoff_ip_transit: - - transit_network_name: IP_TRANSIT_1 - interface_name: FortyGigabitEthernet1/1/1 - virtual_network_name: L3VN1 + - fabric_devices: + fabric_name: Global/USA/SAN-JOSE + device_config: + - device_ip: 10.0.0.1 + borders_settings: + layer3_handoff_ip_transit: + - transit_network_name: IP_TRANSIT_1 + interface_name: FortyGigabitEthernet1/1/1 + virtual_network_name: L3VN1 + + layer3_handoff_sda_transit: + - transit_network_name: SDA_PUB_SUB_TRANSIT - layer3_handoff_sda_transit: - - transit_network_name: SDA_PUB_SUB_TRANSIT + layer2_handoff: + - interface_name: FortyGigabitEthernet1/1/1 + internal_vlan_id: 443 - layer2_handoff: - - interface_name: FortyGigabitEthernet1/1/1 - internal_vlan_id: 443 + - name: Delete the SDA fabric device + cisco.dnac.sda_fabric_devices_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_version: "{{ dnac_version }}" + dnac_log: true + dnac_log_level: DEBUG + dnac_log_append: true + dnac_log_file_path: "{{ dnac_log_file_path }}" + state: deleted + config_verify: true + config: + - fabric_devices: + fabric_name: Global/USA/SAN-JOSE + device_config: + - device_ip: 10.0.0.1 diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 18d9f70788..cc07e67711 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -2161,12 +2161,32 @@ def fn_comp_key(k, dict1, dict2): return dnac_compare_equality(dict1.get(k), dict2.get(k)) +def normalize_ipv6_address(ipv6): + """ + Normalize an IPv6 address for consistent comparison. + """ + if not isinstance(ipv6, str): + raise TypeError("Input must be a string representing an IPv6 address.") + + try: + normalized_address = str(ipaddress.IPv6Address(ipv6)) + return normalized_address + except ValueError: + # self.log("Invalid IPv6 address: {}".format(ipv6)) + return ipv6 # Return as-is if it's not a valid IPv6 address + + def dnac_compare_equality(current_value, requested_value): # print("dnac_compare_equality", current_value, requested_value) if requested_value is None: return True if current_value is None: return True + if isinstance(current_value, str) and isinstance(requested_value, str): + if ":" in current_value and ":" in requested_value: # Possible IPv6 addresses + current_value = normalize_ipv6_address(current_value) + requested_value = normalize_ipv6_address(requested_value) + return current_value == requested_value if isinstance(current_value, dict) and isinstance(requested_value, dict): all_dict_params = list(current_value.keys()) + list(requested_value.keys()) return not any((not fn_comp_key(param, current_value, requested_value) for param in all_dict_params)) diff --git a/plugins/modules/accesspoint_workflow_manager.py b/plugins/modules/accesspoint_workflow_manager.py index f83e8772d9..ed2a01e510 100644 --- a/plugins/modules/accesspoint_workflow_manager.py +++ b/plugins/modules/accesspoint_workflow_manager.py @@ -3259,6 +3259,7 @@ def compare_radio_config(self, current_radio, want_radio): temp_dtos = {} unmatch_count = 0 + self.keymap["power_level"] = "powerlevel" dtos_keys = list(want_radio.keys()) slot_id_key = "_" + str(current_radio["slot_id"]) self.log("Comparing keys for slot ID: {}".format(current_radio["slot_id"]), "INFO") @@ -3289,6 +3290,13 @@ def compare_radio_config(self, current_radio, want_radio): elif dto_key == "radio_band": temp_dtos[self.keymap[dto_key]] = want_radio[dto_key] self.log("Radio band set to: {0}".format(want_radio[dto_key]), "INFO") + elif dto_key == "power_level": + if want_radio[dto_key] != current_radio[self.keymap[dto_key]]: + temp_dtos[self.keymap[dto_key]] = want_radio[dto_key] + self.log("Unmatched key {0}: current value {1}, desired value {2}" + .format(dto_key, current_radio[self.keymap[dto_key]], + want_radio[dto_key]), "INFO") + unmatch_count = unmatch_count + 1 else: if want_radio[dto_key] != current_radio[dto_key]: temp_dtos[self.keymap[dto_key]] = want_radio[dto_key] diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 3a49eac709..5c25455ca9 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -746,8 +746,9 @@ def __init__(self, module): self.deleted_devices, self.provisioned_device_deleted, self.no_device_to_delete = [], [], [] self.response_list, self.role_updated_list, self.device_role_name = [], [], [] self.udf_added, self.udf_deleted = [], [] - self.ip_address_for_update, self.updated_ip = [], [] - self.output_file_name = [] + self.ip_address_for_update, self.updated_ip, self.update_device_ips = [], [], [] + self.output_file_name, self.device_not_exist = [], [] + self.resync_successful_devices, self.device_not_exist_to_resync = [], [] def validate_input(self): """ @@ -1431,7 +1432,7 @@ def resync_devices(self): self.msg = ( "Device(s) '{0}' have been successfully resynced in the inventory in Cisco Catalyst Center. " ).format(resync_successful_devices) - + self.resync_successful_devices.append(resync_successful_devices) if resync_failed_for_all_device: self.status = "failed" self.log(self.msg, "ERROR") @@ -3131,6 +3132,11 @@ def get_diff_merged(self, config): self.log(self.msg, "ERROR") return self + if self.config[0].get("device_resync"): + is_device_exists = self.is_device_exist_in_ccc(config['ip_address_list']) + if not is_device_exists: + self.device_not_exist_to_resync.append(config['ip_address_list']) + if self.config[0].get('update_interface_details'): device_to_update = self.get_device_ips_from_config_priority() device_exist = self.is_device_exist_for_update(device_to_update) @@ -3172,9 +3178,10 @@ def get_diff_merged(self, config): device_exist = self.is_device_exist_for_update(device_to_update) if not device_exist: + self.device_not_exist.append(device_to_update) self.msg = ("Unable to reboot device because the device(s) listed: {0} are not present in the" " Cisco Catalyst Center.").format(str(device_to_update)) - self.status = "failed" + self.status = "ok" self.result['response'] = self.msg self.log(self.msg, "ERROR") return self @@ -3229,7 +3236,10 @@ def get_diff_merged(self, config): device_params.pop('snmpPrivProtocol', None) device_to_add_in_ccc = device_params['ipAddress'] - self.mandatory_parameter(device_to_add_in_ccc).check_return_status() + + if not self.config[0].get("device_resync"): + self.mandatory_parameter(device_to_add_in_ccc).check_return_status() + try: response = self.dnac._exec( family="devices", @@ -3533,7 +3543,7 @@ def get_diff_merged(self, config): if response and isinstance(response, dict): self.check_device_update_execution_response(response, device_ip) - update_device_ips.append(device_ip) + self.update_device_ips.append(device_ip) self.check_return_status() except Exception as e: @@ -4077,6 +4087,17 @@ def update_inventory_profile_messages(self): " operation").format("', '".join(self.no_device_to_delete)) result_msg_list_not_changed.append(deleted_devices) + if self.device_not_exist: + devices = ', '.join(map(str, self.device_not_exist)) + device_not_exist = ("Unable to reboot device because the device(s) listed: {0} are not present in the" + " Cisco Catalyst Center.").format(str(devices)) + result_msg_list_not_changed.append(device_not_exist) + + if self.device_not_exist_to_resync: + devices = ', '.join(map(str, self.device_not_exist_to_resync)) + device_not_exist = ("Unable to resync device because the device(s) listed: {0} are not present in the Cisco Catalyst Center.").format(str(devices)) + result_msg_list_not_changed.append(device_not_exist) + if self.response_list: response_list_for_update = "{0}".format(", ".join(self.response_list)) result_msg_list_changed.append(response_list_for_update) @@ -4104,6 +4125,15 @@ def update_inventory_profile_messages(self): output_file_name = "Device Details Exported Successfully to the CSV file: {0}".format("', '".join(self.output_file_name)) result_msg_list_changed.append(output_file_name) + if self.update_device_ips: + updated_ips = "Device(s) '{0}' present in Cisco Catalyst Center and have been updated successfully.".format(str(self.update_device_ips)) + result_msg_list_changed.append(updated_ips) + + if self.resync_successful_devices: + devices = ', '.join(map(str, self.resync_successful_devices)) + resync_successful_devices = "Device(s) '{0}' have been successfully resynced in the inventory in Cisco Catalyst Center.".format(str(devices)) + result_msg_list_changed.append(resync_successful_devices) + if result_msg_list_not_changed and result_msg_list_changed: self.result["changed"] = True self.msg = "{0}, {1}".format(" ".join(result_msg_list_not_changed), " ".join(result_msg_list_changed)) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 509a83f5e8..18b493c2ce 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -14,10 +14,10 @@ module: ise_radius_integration_workflow_manager short_description: Resource module for Authentication and Policy Servers description: -- Manage operations on Authentication and Policy Servers. -- API to create Authentication and Policy Server Access Configuration. -- API to update Authentication and Policy Server Access Configuration. -- API to delete Authentication and Policy Server Access Configuration. + - Manage operations on Authentication and Policy Servers. + - API to create Authentication and Policy Server Access Configuration. + - API to update Authentication and Policy Server Access Configuration. + - API to delete Authentication and Policy Server Access Configuration. version_added: '6.14.0' extends_documentation_fragment: - cisco.dnac.workflow_manager_params @@ -27,15 +27,15 @@ config_verify: description: Set to True to verify the Cisco Catalyst Center after applying the playbook config. type: bool - default: False + default: false state: description: The state of Cisco Catalyst Center after module completion. type: str - choices: [ "merged", "deleted" ] + choices: ["merged", "deleted"] default: merged config: description: - - List of details of Authentication and Policy Servers being managed. + - List of details of Authentication and Policy Servers being managed. type: list elements: dict required: true @@ -47,134 +47,134 @@ suboptions: server_type: description: - - Type of the Authentication and Policy Server. - - ISE for Cisco ISE servers. - - AAA for Non-Cisco ISE servers. + - Type of the Authentication and Policy Server. + - ISE for Cisco ISE servers. + - AAA for Non-Cisco ISE servers. type: str - choices: [ "AAA", "ISE" ] + choices: ["AAA", "ISE"] default: AAA server_ip_address: description: IP Address of the Authentication and Policy Server. type: str - required: True + required: true shared_secret: description: - - Shared secret between devices and authentication and policy server. - - Shared secret must have 4 to 100 characters with no spaces or the following characters - ["<", "?"]. - - Shared secret is a Read-Only parameter. + - Shared secret between devices and authentication and policy server. + - Shared secret must have 4 to 100 characters with no spaces or the following characters - ["<", "?"]. + - Shared secret is a Read-Only parameter. type: str protocol: description: - - Type of protocol for authentication and policy server. - - RADIUS provides centralized services (AAA) for users in remote access scenarios. - - TACACS focuses on access control and administrative authentication for network devices. + - Type of protocol for authentication and policy server. + - RADIUS provides centralized services (AAA) for users in remote access scenarios. + - TACACS focuses on access control and administrative authentication for network devices. type: str - choices: [ "TACACS", "RADIUS", "RADIUS_TACACS" ] + choices: ["TACACS", "RADIUS", "RADIUS_TACACS"] default: RADIUS encryption_scheme: description: - - Type of encryption scheme for additional security. - - If encryption scheme is given, then message authenticator code and encryption keys need to be required. - - Updation of encryption scheme is not possible. - - > - KEYWRAP is used for securely wrapping and unwrapping encryption keys, - ensuring their confidentiality during transmission or storage. - - > - RADSEC is an extension of RADIUS that provides secure communication - between RADIUS clients and servers over TLS/SSL. Enhances enhancing the - confidentiality and integrity of authentication and accounting data exchange. + - Type of encryption scheme for additional security. + - If encryption scheme is given, then message authenticator code and encryption keys need to be required. + - Updation of encryption scheme is not possible. + - > + KEYWRAP is used for securely wrapping and unwrapping encryption keys, + ensuring their confidentiality during transmission or storage. + - > + RADSEC is an extension of RADIUS that provides secure communication + between RADIUS clients and servers over TLS/SSL. Enhances enhancing the + confidentiality and integrity of authentication and accounting data exchange. type: str - choices: [ "KEYWRAP", "RADSEC" ] + choices: ["KEYWRAP", "RADSEC"] encryption_key: description: - - Encryption key used to encrypt shared secret. - - Updation of encryption scheme is not possible. - - Required when encryption_scheme is provided. - - > - When ASCII format is selected, Encryption Key may contain - alphanumeric and special characters. Key must be 16 char long. + - Encryption key used to encrypt shared secret. + - Updation of encryption scheme is not possible. + - Required when encryption_scheme is provided. + - > + When ASCII format is selected, Encryption Key may contain + alphanumeric and special characters. Key must be 16 char long. type: str message_authenticator_code_key: description: - - Message key used to encrypt shared secret. - - Updation of message key is not possible. - - Required when encryption_scheme is provided. - - > - Message Authentication Code Key may contain alphanumeric and special characters. - Key must be 20 char long. + - Message key used to encrypt shared secret. + - Updation of message key is not possible. + - Required when encryption_scheme is provided. + - > + Message Authentication Code Key may contain alphanumeric and special characters. + Key must be 20 char long. type: str authentication_port: description: - - Authentication port of RADIUS server. - - Updation of authentication port is not possible. - - Authentication port should be from 1 to 65535. + - Authentication port of RADIUS server. + - Updation of authentication port is not possible. + - Authentication port should be from 1 to 65535. type: int default: 1812 accounting_port: description: - - Accounting port of RADIUS server. - - Updation of accounting port is not possible. - - Accounting port should be from 1 to 65535. + - Accounting port of RADIUS server. + - Updation of accounting port is not possible. + - Accounting port should be from 1 to 65535. type: int default: 1813 retries: description: - - Number of communication retries between devices and authentication and policy server. - - Retries should be from 1 to 3. + - Number of communication retries between devices and authentication and policy server. + - Retries should be from 1 to 3. type: int default: 3 timeout: description: - - Number of seconds before timing out between devices and authentication and policy server. - - Timeout should be from 2 to 20. + - Number of seconds before timing out between devices and authentication and policy server. + - Timeout should be from 2 to 20. type: int default: 4 role: description: - - Role of authentication and policy server. - - Updation of role is not possible + - Role of authentication and policy server. + - Updation of role is not possible type: str default: secondary pxgrid_enabled: description: - - Set True to enable the Pxgrid and False to disable the Pxgrid. - - Pxgrid is available only for the Cisco ISE Servers. - - > - PxGrid facilitates seamless integration and information sharing across products, - enhancing threat detection and response capabilities within the network ecosystem. + - Set True to enable the Pxgrid and False to disable the Pxgrid. + - Pxgrid is available only for the Cisco ISE Servers. + - > + PxGrid facilitates seamless integration and information sharing across products, + enhancing threat detection and response capabilities within the network ecosystem. type: bool - default: True + default: true use_dnac_cert_for_pxgrid: description: Set True to use the Cisco Catalyst Center certificate for the Pxgrid. type: bool - default: False + default: false cisco_ise_dtos: description: - - List of Cisco ISE Data Transfer Objects (DTOs). - - Required when server_type is set to ISE. + - List of Cisco ISE Data Transfer Objects (DTOs). + - Required when server_type is set to ISE. type: list elements: dict suboptions: user_name: description: - - User name of the Cisco ISE server. - - Required for passing the cisco_ise_dtos. + - User name of the Cisco ISE server. + - Required for passing the cisco_ise_dtos. type: str password: description: - - Password of the Cisco ISE server. - - Password must have 4 to 127 characters with no spaces or the following characters - "<". - - Required for passing the cisco_ise_dtos. + - Password of the Cisco ISE server. + - Password must have 4 to 127 characters with no spaces or the following characters - "<". + - Required for passing the cisco_ise_dtos. type: str fqdn: description: - - Fully-qualified domain name of the Cisco ISE server. - - Required for passing the cisco_ise_dtos. + - Fully-qualified domain name of the Cisco ISE server. + - Required for passing the cisco_ise_dtos. type: str ip_address: description: - - IP Address of the Cisco ISE Server. - - Required for passing the cisco_ise_dtos. + - IP Address of the Cisco ISE Server. + - Required for passing the cisco_ise_dtos. type: str description: description: Description about the Cisco ISE server. @@ -200,19 +200,19 @@ type: str trusted_server: description: - - Indicates whether the certificate is trustworthy for the server. - - Serves as a validation of its authenticity and reliability in secure connections. - default: True + - Indicates whether the certificate is trustworthy for the server. + - Serves as a validation of its authenticity and reliability in secure connections. + default: true type: bool ise_integration_wait_time: description: - - Indicates the sleep time after initiating the Cisco ISE integration process. - - Maximum sleep time should be less or equal to 120 seconds. + - Indicates the sleep time after initiating the Cisco ISE integration process. + - Maximum sleep time should be less or equal to 120 seconds. default: 20 type: int requirements: -- dnacentersdk >= 2.7.2 -- python >= 3.9 + - dnacentersdk >= 2.7.2 + - python >= 3.9 notes: - SDK Method used are system_settings.SystemSettings.add_authentication_and_policy_server_access_configuration, @@ -1886,11 +1886,11 @@ def main(): # Create an AnsibleModule object with argument specifications module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) ccc_ise_radius = IseRadiusIntegration(module) - if ccc_ise_radius.compare_dnac_versions(ccc_ise_radius.get_ccc_version(), "2.3.5.3") < 0: + if ccc_ise_radius.compare_dnac_versions(ccc_ise_radius.get_ccc_version(), "2.3.7.0") < 0: ccc_ise_radius.msg = ( "The specified version '{0}' does not support the authentication and policy server integration feature. " - "Supported versions start from '2.3.5.3' onwards. " - "Version '2.3.5.3' introduces APIs for creating, updating and deleting the AAA servers and ISE server." + "Supported versions start from '2.3.7.0' onwards. " + "Version '2.3.7.0' introduces APIs for creating, updating and deleting the AAA servers and ISE server." .format(ccc_ise_radius.get_ccc_version()) ) ccc_ise_radius.status = "failed" diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 6955510ee7..dcc2d5ab06 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -537,6 +537,25 @@ - site_name: string name: string +- name: Delete Global Pool + cisco.dnac.network_settings_workflow_manager: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_version: "{{ dnac_version }}" + dnac_log_level: "{{ dnac_log_level }}" + dnac_log: true + state: deleted + config_verify: true + config: + - global_pool_details: + settings: + ip_pool: + - name: string + - name: Manage the network functions cisco.dnac.network_settings_workflow_manager: dnac_host: "{{dnac_host}}" @@ -1308,14 +1327,30 @@ def get_aaa_settings_for_site(self, site_name, site_id): params={"id": site_id} ) # Extract AAA network and client/endpoint settings - network_aaa = aaa_network_response.get("response", {}).get("aaaNetwork") - client_and_endpoint_aaa = aaa_network_response.get("response", {}).get("aaaClient") + response = aaa_network_response.get("response", {}) + network_aaa = response.get("aaaNetwork") + client_and_endpoint_aaa = response.get("aaaClient") if not network_aaa or not client_and_endpoint_aaa: - self.log("No AAA settings found for site '{0}' (ID: {1})".format(site_name, site_id), "WARNING") - return None, None + missing = [] + if not network_aaa: + missing.append("network_aaa") + if not client_and_endpoint_aaa: + missing.append("client_and_endpoint_aaa") + self.log( + "No {0} settings found for site '{1}' (ID: {2})".format( + " and ".join(missing), site_name, site_id + ), + "WARNING", + ) + return network_aaa, client_and_endpoint_aaa - self.log("Successfully retrieved AAA Network settings for site '{0}' (ID: {1}): {2}".format(site_name, site_id, network_aaa), "DEBUG") + self.log( + "Successfully retrieved AAA Network settings for site '{0}' (ID: {1}): {2}".format( + site_name, site_id, network_aaa + ), + "DEBUG", + ) self.log("Successfully retrieved AAA Client and Endpoint settings for site '{0}' (ID: {1}): {2}" .format(site_name, site_id, client_and_endpoint_aaa), "DEBUG") except Exception as e: @@ -1906,30 +1941,34 @@ def get_have_global_pool(self, global_pool_details): global_pool = [] global_pool_index = 0 + errors = [] # To collect all error messages + for pool_details in global_pool_ippool: name = pool_details.get("name") if name is None: - self.msg = "Missing required parameter 'name' in global_pool_details" - self.status = "failed" - return self + errors.append("Missing required parameter 'name' in global_pool_details: {}".format(pool_details)) + continue name_length = len(name) if name_length > 100: - self.msg = "The length of the '{0}' in global_pool_details should be less or equal to 100. Invalid_config: {1}".format(name, pool_details) - self.status = "failed" - return self + errors.append("The length of the 'name' in global_pool_details should be less or equal to 100. Invalid_config: {}".format(pool_details)) if " " in name: - self.msg = "The 'name' in global_pool_details should not contain any spaces." - self.status = "failed" - return self + errors.append("The 'name' in global_pool_details should not contain any spaces. Invalid_config: {}".format(pool_details)) pattern = r'^[\w\-./]+$' if not re.match(pattern, name): - self.msg = "The 'name' in global_pool_details should contain only letters, numbers and -_./ characters." - self.status = "failed" - return self + errors.append("The 'name' in global_pool_details should contain only letters, numbers, and -_./ characters. Invalid_config: {}" + .format(pool_details)) + + if errors: + # If there are errors, return a failure status with all messages + self.msg = "Validation failed with the following errors:\n" + "\n".join(errors) + self.status = "failed" + return self + for pool_details in global_pool_ippool: + name = pool_details.get("name") # If the Global Pool doesn't exist and a previous name is provided # Else try using the previous name global_pool.append(self.global_pool_exists(name)) @@ -2343,8 +2382,6 @@ def get_want_reserve_pool(self, reserve_pool): pool_values.update({"ipv4DnsServers": []}) if pool_values.get("ipv6AddressSpace") is None: pool_values.update({"ipv6AddressSpace": False}) - if pool_values.get("slaacSupport") is None: - pool_values.update({"slaacSupport": True}) if pool_values.get("ipv4TotalHost") is None: del pool_values['ipv4TotalHost'] if pool_values.get("ipv6AddressSpace") is True: diff --git a/plugins/modules/sda_fabric_devices_workflow_manager.py b/plugins/modules/sda_fabric_devices_workflow_manager.py index 97c37a3698..94c6330bb0 100644 --- a/plugins/modules/sda_fabric_devices_workflow_manager.py +++ b/plugins/modules/sda_fabric_devices_workflow_manager.py @@ -66,19 +66,18 @@ - IP address of the device to be added to the fabric site. - Mandatory parameter for all operations under fabric_devices. - Device must be provisioned to the site prior to configuration. + - For deleting a device, the device will be deleted if the user doesnot pass the layer3_handoff_ip_transit, + layer3_handoff_sda_transit and layer2_handoff with the state as deleted. type: str required: true - delete_fabric_device: - description: - - Effective only when the state is deleted. - - Set to true to delete the device from the fabric site, or false to retain it. - type: bool device_roles: description: - Specifies the role(s) of the device within the fabric site. - This parameter is required when adding the device to the fabric site. - The device roles cannot be updated once assigned. - At least one device must be a CONTROL_PLANE_NODE to assign roles to other devices. + - An EDGE_NODE in a fabric cannot be a CONTROL_PLANE_NODE. + - The device role [CONTROL_PLANE_NODE, EDGE_NODE] is not allowed. - Available roles, - CONTROL_PLANE_NODE - Manages the mapping of endpoint IP addresses to their location within the network using LISP, enabling mobility. @@ -695,7 +694,7 @@ interface_name: FortyGigabitEthernet1/1/1 virtual_network_name: L3VN1 -- name: Delete the device along with L2 Handoff and L3 Handoff +- name: Delete the device cisco.dnac.sda_fabric_devices_workflow_manager: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -713,19 +712,6 @@ fabric_name: Global/USA/SAN-JOSE device_config: - device_ip: 10.0.0.1 - delete_fabric_device: true - borders_settings: - layer3_handoff_ip_transit: - - transit_network_name: IP_TRANSIT_1 - interface_name: FortyGigabitEthernet1/1/1 - virtual_network_name: L3VN1 - - layer3_handoff_sda_transit: - transit_network_name: SDA_PUB_SUB_TRANSIT - - layer2_handoff: - - interface_name: FortyGigabitEthernet1/1/1 - internal_vlan_id: 550 """ RETURN = r""" @@ -970,7 +956,6 @@ def validate_input(self): "type": 'list', "elements": 'dict', "device_ip": {"type": 'string'}, - "delete_fabric_device": {"type": 'bool'}, "device_roles": { "type": 'list', "elements": 'str', @@ -2170,7 +2155,7 @@ def get_have_fabric_devices(self, fabric_devices): fabric_site_id = self.get_fabric_zone_id_from_name(fabric_name, site_id) if not fabric_site_id: self.msg = ( - "The provided 'fabric_name' '{fabric_name}' is not valid a fabric site." + "The provided 'fabric_name' '{fabric_name}' is not a valid fabric site." .format(fabric_name=fabric_name) ) if self.params.get("state") == "deleted": @@ -2212,7 +2197,6 @@ def get_have_fabric_devices(self, fabric_devices): "id": None, "fabric_site_id": None, "network_device_id": None, - "delete_fabric_device": False, } # Fabric device IP is mandatory for this workflow @@ -2269,14 +2253,9 @@ def get_have_fabric_devices(self, fabric_devices): "skipping provisioning checks." ) - delete_fabric_device = item.get("delete_fabric_device") - if delete_fabric_device is None: - delete_fabric_device = False - fabric_devices_info.update({ "fabric_site_id": fabric_site_id, "network_device_id": network_device_id, - "delete_fabric_device": delete_fabric_device, }) device_info = self.fabric_device_exists(fabric_site_id, network_device_id, fabric_device_ip) fabric_devices_info.update({ @@ -2481,6 +2460,16 @@ def get_device_params(self, fabric_id, network_id, device_details, config_index) # 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 sorted(device_roles) == ["CONTROL_PLANE_NODE", "EDGE_NODE"]: + self.msg = ( + "The current combination of roles {device_roles} is invalid. " + "An EDGE_NODE in a fabric cannot be a CONTROL_PLANE_NODE." + .format(device_roles=device_roles) + ) + self.log(self.msg, "ERROR") + self.status = "failed" + self.check_return_status() + if not have_device_exists: if not device_roles: self.msg = ( @@ -4604,8 +4593,8 @@ def delete_ip_l3_handoff(self, have_ip_l3_handoff, device_ip, return self result_fabric_device_response.get("ip_l3_handoff_details").update({ - "Deleted L2 Handoff": delete_ip_l3_handoff, - "Non existing L2 Handoff": non_existing_ip_l3_handoff + "Deleted L3 Handoff": delete_ip_l3_handoff, + "Non existing L3 Handoff": non_existing_ip_l3_handoff }) if delete_ip_l3_handoff: result_fabric_device_msg.update({ @@ -4632,7 +4621,7 @@ def delete_fabric_devices(self, fabric_devices): Description: Do the basic validation. If L2 Handoff is available, call the API 'delete_l2_handoff'. If SDA L3 Handoff is available, call the API 'delete_sda_l3_handoff'. If IP L3 Handoff is availabl, call the API - 'delete_ip_l3_handoff'. If delete_fabric_device is set to True, call the API 'delete_fabric_device_by_id' + 'delete_ip_l3_handoff'. If only the device IP is provided, call the API 'delete_fabric_device_by_id' to delete the fabric device from the fabric site. """ @@ -4708,12 +4697,21 @@ def delete_fabric_devices(self, fabric_devices): "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") - # If the delete_fabric_device is set to True + # If 'sda_l3_handoff_details' and 'l3_sda_handoff' and 'l2_handoff' are not provided # We need to delete the device as well along with the settings - if delete_fabric_device: + + layer3_handoff_ip_transit = None + layer3_handoff_sda_transit = None + layer2_handoff = None + borders_settings = item.get("borders_settings") + if borders_settings: + layer3_handoff_ip_transit = item.get("layer3_handoff_ip_transit") + layer3_handoff_sda_transit = item.get("layer3_handoff_sda_transit") + layer2_handoff = item.get("layer2_handoff") + + if not (layer3_handoff_ip_transit or layer3_handoff_sda_transit or layer2_handoff): if device_exists: id = have_fabric_device.get("id") self.log( @@ -5003,7 +5001,6 @@ def verify_diff_deleted(self, config): fabric_device_index = -1 for item in device_config: fabric_device_index += 1 - delete_fabric_device = self.have.get("fabric_devices")[fabric_device_index].get("delete_fabric_device") device_ip = item.get("device_ip") fabric_device_details = self.have.get("fabric_devices")[fabric_device_index] if item.get("layer3_handoff_ip_transit"): @@ -5054,7 +5051,9 @@ def verify_diff_deleted(self, config): .format(ip=device_ip), "INFO" ) - if delete_fabric_device: + if not (item.get("layer3_handoff_ip_transit") or + item.get("layer3_handoff_sda_transit") or + item.get("layer2_handoff")): # Verifying the absence of the device if item.get("device_details"): diff --git a/plugins/modules/sda_fabric_transits_workflow_manager.py b/plugins/modules/sda_fabric_transits_workflow_manager.py index 02942c4eeb..f5d263afa6 100644 --- a/plugins/modules/sda_fabric_transits_workflow_manager.py +++ b/plugins/modules/sda_fabric_transits_workflow_manager.py @@ -14,10 +14,10 @@ module: sda_fabric_transits_workflow_manager short_description: Resource module for SDA fabric transits description: -- Manage operations on SDA fabric transits. -- API to create transit networks. -- API to update transit networks. -- API to delete transit networks. + - Manage operations on SDA fabric transits. + - API to create transit networks. + - API to update transit networks. + - API to delete transit networks. version_added: '6.18.0' extends_documentation_fragment: - cisco.dnac.workflow_manager_params @@ -27,16 +27,16 @@ config_verify: description: Set to True to verify the Cisco Catalyst Center after applying the playbook config. type: bool - default: False + default: false state: description: The state of Cisco Catalyst Center after module completion. type: str - choices: [ merged, deleted ] + choices: [merged, deleted] default: merged config: description: - - A list of SDA fabric transit configurations. - - Each entry in the list represents a transit network configuration. + - A list of SDA fabric transit configurations. + - Each entry in the list represents a transit network configuration. type: list elements: dict required: true @@ -48,9 +48,9 @@ suboptions: name: description: - - The name of the SDA fabric transit. - - It facilitates seamless communication between different network segments. - - Required for the operations in the SDA fabric transits. + - The name of the SDA fabric transit. + - It facilitates seamless communication between different network segments. + - Required for the operations in the SDA fabric transits. type: str transit_type: description: Type of the fabric tranist. @@ -65,9 +65,9 @@ type: str ip_transit_settings: description: - - The configuration settings for IP based transit. - - Required when the type is set to IP_BASED_TRANSIT. - - IP_BASED_TRANSIT cannot be updated. + - The configuration settings for IP based transit. + - Required when the type is set to IP_BASED_TRANSIT. + - IP_BASED_TRANSIT cannot be updated. type: dict suboptions: routing_protocol_name: @@ -77,38 +77,38 @@ choices: [BGP] autonomous_system_number: description: - - Used by routing protocols like BGP to manage routing between different autonomous systems. - - Autonomous System Number (ANS) should be from 1 to 4294967295. - - The ASN should be unique for every IP-based transits. - - Required when the transit_type is set to IP_BASED_TRANSIT. + - Used by routing protocols like BGP to manage routing between different autonomous systems. + - Autonomous System Number (ANS) should be from 1 to 4294967295. + - The ASN should be unique for every IP-based transits. + - Required when the transit_type is set to IP_BASED_TRANSIT. type: str sda_transit_settings: description: - - The configuration settings for SDA-based transit. - - Required when the transit_type is set to SDA_LISP_PUB_SUB_TRANSIT or SDA_LISP_BGP_TRANSIT. + - The configuration settings for SDA-based transit. + - Required when the transit_type is set to SDA_LISP_PUB_SUB_TRANSIT or SDA_LISP_BGP_TRANSIT. type: dict suboptions: is_multicast_over_transit_enabled: description: - - Determines whether multicast traffic is permitted to traverse the transit network. - - Enabling this option allows the distribution of data to multiple recipients across different network segments. - - Available only when the transit type is set to SDA_LISP_PUB_SUB_TRANSIT. + - Determines whether multicast traffic is permitted to traverse the transit network. + - Enabling this option allows the distribution of data to multiple recipients across different network segments. + - Available only when the transit type is set to SDA_LISP_PUB_SUB_TRANSIT. type: bool control_plane_network_device_ips: description: - - Specifies the IP addresses of the network devices that form the control plane. - - Required when the transit_type is set to either SDA_LISP_BGP_TRANSIT or SDA_LISP_PUB_SUB_TRANSIT. - - Atleast one control plane network device is required. - - A maximum of 2 control plane network devices are allowed when the transit_type is SDA_LISP_BGP_TRANSIT. - - A maximum of 4 control plane network devices are allowed when the transit_type is SDA_LISP_PUB_SUB_TRANSIT. - - SDA_LISP_PUB_SUB_TRANSIT supports only devices with IOS XE 17.6 or later. - - The devices must be present in the Fabric site or zone. + - Specifies the IP addresses of the network devices that form the control plane. + - Required when the transit_type is set to either SDA_LISP_BGP_TRANSIT or SDA_LISP_PUB_SUB_TRANSIT. + - Atleast one control plane network device is required. + - A maximum of 2 control plane network devices are allowed when the transit_type is SDA_LISP_BGP_TRANSIT. + - A maximum of 4 control plane network devices are allowed when the transit_type is SDA_LISP_PUB_SUB_TRANSIT. + - SDA_LISP_PUB_SUB_TRANSIT supports only devices with IOS XE 17.6 or later. + - The devices must be present in the Fabric site or zone. type: list elements: str requirements: -- dnacentersdk >= 2.9.2 -- python >= 3.9 + - dnacentersdk >= 2.9.2 + - python >= 3.9 notes: - SDK Method used are devices.Devices.get_device_list, @@ -716,7 +716,7 @@ def handle_ip_transit_settings(self, item, fabric_transits_values, fabric_transi .get("details").get("ipTransitSettings") autonomous_system_number = item.get("ip_transit_settings").get("autonomous_system_number") - if not autonomous_system_number: + if autonomous_system_number is None: self.msg = "The required parameter 'autonomous_system_number' in 'ip_transit_settings' is missing." self.status = "failed" return self.check_return_status() diff --git a/tests/integration/ccc_device_credential_management/tests/test_assign_credentials.yml b/tests/integration/ccc_device_credential_management/tests/test_assign_credentials.yml index 5161ff9d23..3e89419575 100644 --- a/tests/integration/ccc_device_credential_management/tests/test_assign_credentials.yml +++ b/tests/integration/ccc_device_credential_management/tests/test_assign_credentials.yml @@ -106,8 +106,8 @@ assert: that: - item.changed == true - - "'Created Successfully' in item.response[0].globalCredential.Creation.msg" - - item.response[0].globalCredential.Validation == "Success" + - "'Created Successfully' in item.response[0].global_credential.Creation.msg" + - item.response[0].global_credential.Validation == "Success" loop: "{{ result_create_credentials.results }}" when: result_create_credentials is defined @@ -135,7 +135,7 @@ assert: that: - item.changed == true - - "'Device Credential Assigned to a site is Successfully' in item.response[0].assignCredential['Assign Credentials'].msg" + - "'Device Credential Assigned to a site is Successfully' in item.response[0].assign_credential['Assign Credentials'].msg" loop: "{{ result_assign_credentials.results }}" when: result_assign_credentials is defined @@ -218,7 +218,7 @@ assert: that: - item.changed == false - - "'No device available in the site' in item.response[0].applyCredential['No Apply Credentials'].msg" + - "'No device available in the site' in item.response[0].apply_credential['No Apply Credentials'].msg" loop: "{{ result_apply_credentials.results }}" when: result_apply_credentials is defined @@ -272,7 +272,7 @@ assert: that: - item.changed == true - - "'deleted successfully' in item.response" + - "'deleted successfully' in item.msg" loop: "{{ result_delete_site.results }}" when: result_delete_site is defined @@ -300,7 +300,7 @@ assert: that: - item.changed == true - - "'Deleted Successfully' in item.response[0].globalCredential.Deletion.msg" - - item.response[0].globalCredential.Validation == "Success" + - "'Deleted Successfully' in item.response[0].global_credential.Deletion.msg" + - item.response[0].global_credential.Validation == "Success" loop: "{{ result_delete_credentials.results }}" when: result_delete_credentials is defined diff --git a/tests/integration/ccc_device_credential_management/tests/test_device_credential_management.yml b/tests/integration/ccc_device_credential_management/tests/test_device_credential_management.yml index f4f9af6f3e..7eb7531ec6 100644 --- a/tests/integration/ccc_device_credential_management/tests/test_device_credential_management.yml +++ b/tests/integration/ccc_device_credential_management/tests/test_device_credential_management.yml @@ -61,8 +61,8 @@ assert: that: - item.changed == true - - "'Created Successfully' in item.response[0].globalCredential.Creation.msg" - - item.response[0].globalCredential.Validation == "Success" + - "'Created Successfully' in item.response[0].global_credential.Creation.msg" + - item.response[0].global_credential.Validation == "Success" loop: "{{ result_create_credentials.results }}" when: result_create_credentials is defined @@ -90,8 +90,8 @@ assert: that: - item.changed == true - - "'Updated Successfully' in item.response[0].globalCredential.Updation.msg" - - item.response[0].globalCredential.Validation == "Success" + - "'Updated Successfully' in item.response[0].global_credential.Updation.msg" + - item.response[0].global_credential.Validation == "Success" loop: "{{ result_update_credentials.results }}" when: result_update_credentials is defined @@ -119,7 +119,7 @@ assert: that: - item.changed == true - - "'Deleted Successfully' in item.response[0].globalCredential.Deletion.msg" - - item.response[0].globalCredential.Validation == "Success" + - "'Deleted Successfully' in item.response[0].global_credential.Deletion.msg" + - item.response[0].global_credential.Validation == "Success" loop: "{{ result_delete_credentials.results }}" when: result_delete_credentials is defined diff --git a/tests/integration/ccc_inventory_management/tests/test_inventory_management.yml b/tests/integration/ccc_inventory_management/tests/test_inventory_management.yml index 66a100b3eb..b083253876 100644 --- a/tests/integration/ccc_inventory_management/tests/test_inventory_management.yml +++ b/tests/integration/ccc_inventory_management/tests/test_inventory_management.yml @@ -71,7 +71,7 @@ assert: that: - item.changed == true - - "'added to Cisco Catalyst Center' in item.msg" + - "'added successfully in Cisco Catalyst Center.' in item.msg" loop: "{{ result_add_device.results }}" when: result_add_device is defined